Использование операторов приращения постфикса / префикса в составных литералах в C99

#c99 #postfix-operator #compound-literals #prefix-operator

#c99 #postfix-operator #составные литералы #префиксный оператор

Вопрос:

Существует пример, заимствованный из CARM (Справочное руководство по CA, Сэмюэл П. Харбисон III, Гай Л. Стил-младший, 2002, Prentice Hall), стр. 218-219. Я пишу все три фрагмента кода в одном источнике:

 #include <stdio.h>

void f1(){
    int *p[5];
    int i=0;
    m:
    p[i]=(int [1]){i};
    if(  i<5)goto m;
    printf("f1: ");
    for(i=0;i<5;  i)
        printf("p[%d]=%d ",i,*(p[i]));
    printf("n");
    fflush(stdout);
}

void f2(){
    int *p[5];
    int i=0;
    p[i]=(int [1]){i  };
    p[i]=(int [1]){i  };
    p[i]=(int [1]){i  };
    p[i]=(int [1]){i  };
    p[i]=(int [1]){i  };
    printf("f2: ");
    for(i=0;i<5;  i)
        printf("p[%d]=%d ",i,*(p[i]));
    printf("n");
    fflush(stdout);
}

void f3(){
    int *p[5];
    int i;
    for(i=0;i<5;i  ){
        p[i]=(int [1]){i};
    }
    printf("f3: ");
    for(i=0;i<5;  i)
        printf("p[%d]=%d ",i,*(p[i]));
    printf("n");
    fflush(stdout);
}

int main(){ f1(); f2(); f3(); }
 

функция f2 не работает должным образом:

 user@debian:~/test7$ gcc -std=c11 ./carm_1.c -o carm_1
user@debian:~/test7$ ./carm_1
f1: p[0]=4 p[1]=4 p[2]=4 p[3]=4 p[4]=4 
f2: p[0]=-2106668384 p[1]=-2106668408 p[2]=32765 p[3]=2 p[4]=3 
f3: p[0]=4 p[1]=4 p[2]=4 p[3]=4 p[4]=4
 

Но когда я переписал его:

 #include <stdio.h>

void f1(){
    int *p[5];
    int i=0;
    m:
    p[i]=(int [1]){i};
    if(  i<5)goto m;
    printf("f1: ");
    for(i=0;i<5;  i)
        printf("p[%d]=%d ",i,*(p[i]));
    printf("n");
    fflush(stdout);
}

void f2(){
    int *p[5];
    int i=-1;
    p[i]=(int [1]){  i};
    p[i]=(int [1]){  i};
    p[i]=(int [1]){  i};
    p[i]=(int [1]){  i};
    p[i]=(int [1]){  i};
    printf("f2: ");
    for(i=0;i<5;  i)
        printf("p[%d]=%d ",i,*(p[i]));
    printf("n");
    fflush(stdout);
}

void f3(){
    int *p[5];
    int i;
    for(i=0;i<5;i  ){
        p[i]=(int [1]){i};
    }
    printf("f3: ");
    for(i=0;i<5;  i)
        printf("p[%d]=%d ",i,*(p[i]));
    printf("n");
    fflush(stdout);
}

int main(){ f1(); f2(); f3(); }
 

функция f2 работает нормально:

 user@debian:~/test7$ gcc -std=c11 ./carm_2.c -o carm_2
user@debian:~/test7$ ./carm_2
f1: p[0]=4 p[1]=4 p[2]=4 p[3]=4 p[4]=4 
f2: p[0]=0 p[1]=1 p[2]=2 p[3]=3 p[4]=4 
f3: p[0]=4 p[1]=4 p[2]=4 p[3]=4 p[4]=4
 

Почему ? Эти два варианта функции f2 отличаются значением, возвращаемым приращением постфикса / инфикса i (в составном литерале). В первом случае это временное значение. Результат оператора приращения постфикса не равен lvalue . И результат оператора приращения префикса также не является значением lvalue (в соответствии со стр. 226 CARM). Пожалуйста, помогите мне понять. (извините за мой английский).

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

1. Мой вопрос заключается в том, существует ли точка последовательности после инициализатора составного литерала. Если нет, то обе версии f2 являются неопределенным поведением. Если есть, то ошибка в оригинале f2 заключается в том, что постинкремент происходит до p[i] того, как вычисляется, и оригинал f2 выполняет p[1] = (int [1]){0}; ... p[5] = (int [1]){4}; , что, очевидно, UB, поскольку p[0] никогда не инициализируется и p[5] выходит за рамки. Но ваша версия исправит эту ошибку и правильно сделает p[0] = (int [1]){0}; ... p[4] = (int [1]){4}; .

2. В стандарте есть строка о том, что инициализатор, который не является частью составного литерала, является полным выражением и, следовательно, имеет точку последовательности, но это здесь не применимо, поскольку наш инициализатор является частью составного литерала. Тем не менее, я не могу точно сказать, есть ли какое-то другое условие, которое создало бы точку последовательности в этом случае. Но в любом случае, я не думаю, что это имеет какое-либо отношение к проблемам с lvalue / rvalue.

3. clang выдает предупреждения о «непоследовательном изменении и доступе», так что, возможно, это правильно, и точки последовательности нет. (однако gcc не выдает никаких предупреждений, хотя это происходит при некоторых более простых ошибках точки последовательности, например p[i] = i ; ).

4. Спасибо. Да, clang выдает предупреждения: user@debian:~/test7$ clang -std=c11 ./carm_1.c -o carm_11 ./carm_1.c:19:18: warning: unsequenced modification and access to 'i' [-Wunsequenced] p[i]=(int [1]){i }; ~ ^ ... 5 warnings generated. и код не работает: user@debian:~/test7$ ./carm_11 f1: p[0]=4 p[1]=4 p[2]=4 p[3]=4 p[4]=4 f2: p[0]=0 p[1]=0 p[2]=1 p[3]=2 p[4]=3 f3: p[0]=4 p[1]=4 p[2]=4 p[3]=4 p[4]=4

5. Ну, конечно, неопределенное поведение не определено и может делать все, что угодно, включая то, что вы хотите. Возможно, что компилятор выполняет приращение i перед другим доступом к нему, и что неинициализированный p[0] файл содержит указатель на 0 , и что запись в p[5] не вызывает сбоя. Если все это произойдет, то код будет вести себя так, как вы видите.

Ответ №1:

Я не думаю, что это проблема с lvalues и временными значениями; скорее это несвязанная ошибка в примере H amp; S.

В инструкции p[i]=(int [1]){i }; возникает сложный вопрос о том, существует ли точка последовательности после i , которая, по-видимому, зависит от того, является ли i это полным выражением. В C17 явно указано, что инициализатор, который не является частью составного литерала, является полным выражением, что, по-видимому, подразумевает, что i это не полное выражение и что нет точки последовательности. В этом случае рассматриваемое утверждение будет иметь неопределенное поведение, как p[i]=(int [1]){ i}; и в вашей версии.

Однако в C99, похоже, не было исключения «не является частью составного литерала», поэтому я не совсем уверен, какова была ситуация. Но даже если после побочного эффекта есть точка последовательности i , все же, насколько я знаю, порядок вычисления левой и правой сторон = не указан. Таким образом, если компилятор решит сначала оценить правую часть (включая ее побочные эффекты), оператор становится p[1] = (int [1]){0}; и остается p[0] неинициализированным, вызывая неопределенное поведение при разыменовании. По тому же принципу последний оператор становится p[5] = (int [1]){4} , который также является UB, поскольку p имеет длину 5.

Для компилятора, который последовательно выбирает этот порядок, ваш код будет работать; но для компилятора, который выбрал другой порядок, ваш код может стать p[-1] = (int [1]){0} , который аналогично UB . Так что я не думаю, что ваша версия тоже строго правильная.

H amp; S, вероятно, не следовало пытаться быть таким умным и просто писать

 int i=0;
p[i] = (int [1]){i};
i  ;
p[i] = (int [1]){i};
i  ;
p[i] = (int [1]){i};
i  ;
p[i] = (int [1]){i};
i  ;
p[i] = (int [1]){i};
i  ;
 

Тогда код был бы правильным и делал бы то, что они говорят: p[0], ..., p[4] содержал бы пять разных указателей, все указывающие на int s, время жизни которых продолжается в printf цикле, и так, что *(p[0]) == 0 , *(p[1]) == 1 , и т.д.