почему предварительное приращение работает с указателем, но не с последующим приращением

#c

Вопрос:

Я наткнулся на это, решая упражнение:

 void    ft_striteri(char *s, void (*f)(unsigned int, char*))
{
    unsigned int    i;

    if (!s || !f)
        return ;
    i = 0;
    while (s[i  ])
        f(i, s   i);
}
 

Почему приращение post в while не работает, если я делаю это:

 void    ft_striteri(char *s, void (*f)(unsigned int, char*))
{
    unsigned int    i;

    if (!s || !f)
        return ;
    i = -1;
    while (s[  i])
        f(i, s   i);
}
 

Это работает?

Я новичок и все еще очень смущен всей концепцией указателя, есть ли здесь какой-либо нюанс, о котором я не знаю?

Комментарии:

1. Подумайте о том, какое значение i имеет значение при f вызове.

2. Это вопрос не столько об указателях , сколько о значении , возвращаемом i versus i .

3. @paddy: Значения, которые i и i вычисляют (не возвращают), не являются проблемой, потому что инициализация i настраивается для компенсации. Проблема заключается в значении, которое они оставляют i .

4. То, что вы хотите сделать здесь, на английском языке, «Вызовите f для каждой позиции символа в строке s , отличной от нуля, то есть до, но не включая завершающий нулевой символ». Таким образом, ясный и очевидный способ сделать это был бы for(i = 0; s[i] != ''; i ) { f(i, s i); } . Или, немного сокращенно, for(i = 0; s[i]; i ) { f(i, s i); } . (Для этих for циклов не имеет значения, используете ли вы i или i ; они эквивалентны.) Из двух опубликованных вами фрагментов один — более запутанный способ, который работает, а другой — более запутанный способ, который не работает.

5. Они удалили и — из языка Swift. Я понимаю, почему 🙂

Ответ №1:

Проблема заключается в совпадении между сравнением и вызовом функции.
Рассмотрим первую итерацию. В первом фрагменте это будет:

 if(!s[0]) break;
f(1, s   1);
 

Во втором фрагменте это было бы:

 if(!s[0]) break;
f(0, s   0);
 

Ответ №2:

Если вы выполните простую отладку, вы увидите, в чем проблема.

 void    ft_striteri(char *s, void (*f)(unsigned int, char*))
{
    unsigned int    i;

    i = 0;
    while (s[i  ])
    {
        printf("i = %dn", i);
        if(f) f(i, s   i);
    }
}

void    ft_striteri1(char *s, void (*f)(unsigned int, char*))
{
    unsigned int    i;

    i = -1;
    while (s[  i])
    {
        printf("i = %dn", i);
        if(f) f(i, s   i);
    }
}

int main()
{
    ft_striteri("Hello", NULL);
    printf("n");
    ft_striteri1("Hello", NULL);
}
 

https://godbolt.org/z/cqb1aMGje
Результат:

 i = 1
i = 2
i = 3
i = 4
i = 5

i = 0
i = 1
i = 2
i = 3
i = 4
 

Функция с postincrement выполняет итерацию от индекса от 1 до 5 вместо от 0 до 4.

Но обе ваши функции не используют правильный тип для индексов. Это должно быть size_t вместо int .

Я бы лично написал другой способ, проверив «положительный» тест, если параметры в порядке и имеют только одну точку возврата:

 void ft_striteri(char *s, void (*f)(unsigned int, char*))
{
    size_t i = 0;

    if(s amp;amp; f)
    {
        while (s[i])
        {
            f(i, s   i);
            i  ; // or   i; - it makes no difference
        }
    }
}
 

Ответ №3:

Операторы приращения в C указывают, что в дополнение к значению, вычисляемому выражением, возникает побочный эффект.

Вы можете подумать, что это как-то особенное или свойственное этим операторам, но на самом деле это не так. Операторы присваивания также работают таким образом.

Например, (EXPR) эквивалентно (EXPR) = (EXPR) 1 , за исключением того, что EXPR вычисляется только один раз.

Оба этих выражения вычисляют результат (EXPR) 1 , а также имеют побочный эффект сохранения этого значения обратно (EXPR) .

Побочный эффект возникает в любое время во время вычисления полного выражения, которое содержит это подвыражение.

Учитывая:

 while (s[i  ])
    f(i, s   i);
 

Здесь у нас есть два полных выражения: управляющее выражение while цикла, s[i ] , является полным выражением, как и f(i, s i) вызов функции.

Побочные эффекты, требуемые каждым полным выражением, устраняются до вычисления следующего полного выражения. Предыдущее полное выражение видит предыдущее значение, а последующее полное выражение видит новое значение.

Другими словами, i здесь означает:

  1. Вычислите значение i , а также поместите i 1 в i .
  2. Убедитесь, что это i обновление выполняется когда-нибудь во время вычисления полного выражения s[i ] .

Следовательно, выражение f(i, s i) будет учитывать новое значение i , а не предыдущее значение i , которое использовалось для вычисления s[i] . Вызову функции будет присвоен не символ, который был проверен как ненулевой, а следующий символ после него.

Важным фактом здесь является то, что побочные эффекты упорядочиваются на уровне отдельных полных выражений, а не операторов. Здесь i не означает «увеличивать i после каждой итерации всего while цикла, i продолжая ссылаться на старое значение».Если бы это работало таким образом, код работал бы; но это не так.

Таким образом, ваше пересмотренное утверждение исправило согласованность:

 while (s[  i])
    f(i, s   i);
 

потому что здесь i означает:

  1. Вычислите значение i 1 , а также поместите это значение в i .
  2. То же (2), что и выше.

Здесь управляющее выражение while тестов s[i] where i является новым увеличенным значением i ; и вызов функции f(i, s i) ссылается на то же i самое . Управляющее выражение и вызов функции согласованы: они работают с одним и тем же символом строки.

Вы должны были компенсировать предварительное увеличение, инициализируя i значение -1.

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

 // misconception:                // similar idea, correct:
i = 0;
while (s[i  ])                   for (i = 0; s[i]; i  )
    f(i, s   i);                   f(i, s   i);
 

for Цикл позволяет нам иметь своего рода «постинкремент» на уровне всего оператора: в синтаксисе head есть место, где мы можем указать выражения приращения, которые будут вычисляться после всего тела.

Кстати, поскольку i is unsigned int (который также может быть указан как unsigned , без int ), этот тип фактически не имеет значения -1 в своем диапазоне. Когда мы делаем это:

 unsigned int x = -1; // initialize or assign a -1 value to unsigned
 

отрицательное значение уменьшается до наименьшего положительного остатка по модулю UINT_MAX 1 , а результирующее значение — это то, что фактически присвоено.

Значение -1 переходит в UINT_MAX . Итак, вы действительно делаете это:

 i = UINT_MAX; // same meaning as i = -1.
 

это работает, потому что, если i есть unsigned и содержит максимальное значение UINT_MAX , когда мы затем увеличиваем i , оно стремится к нулю. Эта арифметика по модулю или «перенос» является частью определения unsigned типа; это указано в стандарте языка. В другом направлении unsigned аналогично происходит уменьшение нулевого значения UINT_MAX .

Кроме того, из соображений стиля, при обращении к массивам не смешивайте обозначения ptr index и ptr[index] . Это лучше:

 // while the character isn't null, pass a pointer to that
// same character to f:

while (s[  i])
    f(i, amp;s[i]); // address of the character
 

Это amp;s[i] означает amp;*(s i) , что amp;* комбинация операторов («адрес разыменованного указателя») «алгебраически отменяет» оставление s i ; это не менее эффективно.

Эта рекомендация особенно актуальна , если функция f работает с этим одним символом s[i] , а не со всей s i подстрокой s . amp;array[index] Обозначение обычно используется (как правило), когда акцент делается на конкретном элементе массива.

Как читатель C, вы, конечно, не можете доверять этому: amp;array[index] в чужой программе может быть использовано для вычисления значения, которое функция затем использует для доступа к другим элементам массива, а не только к этому. Тем не менее, как автор C, вы можете сделать свой код «похожим на то, что он делает», чтобы было меньше подводных камней для кого-то другого.

Ответ №4:

Я не знаю о нюансах, но вот эквивалент вашего первого:

 void    first(char *s, void (*f)(unsigned int, char*))
{
    unsigned int    i;

    if (!s || !f)
        return ;
    i = 0;
    while (s[i]) {
        f(i, s   i   1);
        s = s   1;
    }
}
 

и второе:

 void    second(char *s, void (*f)(unsigned int, char*))
{
    unsigned int    i;

    if (!s || !f)
        return ;
    i = -1;
    while (s[i 1]) {
        f(i, s   i   1);
        i = i   1;
    }
}
 

На самом деле, ни один из них не выглядит правильным для меня; Я бы подумал, что вы хотели бы:

 void    ft_striteri(char *s, void (*f)(unsigned int, char*))
{
    unsigned int    i;

    if (!s || !f)
        return ;
    i = 0
    while (s[i]) {
        f(i, s   i);
        i  
    }
}
 

что в идиоматическом стиле может быть:

 void    ft_striteri(char *s, void (*f)(unsigned int, char*))
{
    int c;

    if (!s || !f)
        return ;

    for (; *s; s  )
        f(i, s);
}