在 C99 的复合文本中使用后缀/前缀增量运算符

Using postfix/prefix increment operators in compound literals in C99

提问人:al- 提问时间:1/2/2021 最后编辑:al- 更新时间:1/12/2021 访问量:186

问:

有一个例子借鉴了 CARM(C A 参考手册,Samuel P. Harbison III,Guy L. Steele Jr.,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 的后缀/中缀增量(在复合文字中)返回。在第一种情况下,它是临时值。后缀增量运算符的结果不是左值。并且前缀增量运算符的结果也不是左值(根据 CARM 的第 226 页)。请帮我理解。(对不起我的英语)。

c99 后缀运 复合文字 前缀运算符

评论

1赞 Nate Eldredge 1/4/2021
我的问题是复合文字的初始值设定项之后是否有序列点。如果不是,则这两个版本都是未定义的行为。如果有,那么原始中的错误是,在评估之前发生了后增量,并且原始正在执行此操作,这显然是 UB,因为从未初始化并且超出了范围。但是您的版本将修复该错误并正确执行.f2f2p[i]f2p[1] = (int [1]){0}; ... p[5] = (int [1]){4};p[0]p[5]p[0] = (int [1]){0}; ... p[4] = (int [1]){4};
1赞 Nate Eldredge 1/4/2021
标准中有一行,属于复合文本的初始值设定项是一个完整的表达式,因此具有序列点,但这不适用于这里,因为我们的初始值设定项复合文本的一部分。尽管如此,我还是无法确定在这种情况下是否还有其他条件会创建序列点。但无论哪种方式,我都不认为它与左值/右值问题有任何关系。
1赞 Nate Eldredge 1/4/2021
Clang 给出了关于“未排序的修改和访问”的警告,所以也许它是对的,没有序列点。(然而,GCC没有给出警告,尽管它对一些更简单的序列点错误有警告,例如)。p[i] = i++;
0赞 al- 1/4/2021
谢谢。是的,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
1赞 Nate Eldredge 1/4/2021
当然 - 未定义的行为是未定义的,可以做任何事情,包括你想要的。编译器可能碰巧在对它进行其他访问之前执行增量,并且未初始化的恰好包含指向 的指针,并且写入不会导致崩溃。如果所有这些事情都发生了,那么代码的行为将如您所见。ip[0]0p[5]

答:

1赞 Nate Eldredge 1/4/2021 #1

我不认为这是一个关于左值和临时值的问题;而是 H&S 示例中一个不相关的错误。

在语句中,有一个棘手的问题,即 之后是否有一个序列点,这似乎取决于是否是一个完整的表达式。在 C17 中明确指出,不属于复合文字的初始值设定项是一个完整表达式,这似乎意味着 不是完整表达式并且没有序列点。在这种情况下,有问题的语句将是未定义的行为,就像您的版本中一样。p[i]=(int [1]){i++};i++i++i++p[i]=(int [1]){++i};

但是,C99 似乎没有“不属于复合字面”的例外,所以我不太确定那里的情况。但即使副作用后有一个序列点,据我所知,左右两侧的评价顺序也是没有具体说明的。因此,如果编译器选择首先计算右侧(包括其副作用),则语句将变为未初始化状态并保持未初始化状态,从而在取消引用时导致未定义的行为。出于同样的原因,最后一个语句也变为 UB,因为长度为 5。i++=p[1] = (int [1]){0};p[0]p[5] = (int [1]){4}p

对于始终选择该排序的编译器,您的代码将起作用;但是对于选择其他顺序的编译器,您的代码可能会变成 UB。所以我认为你的版本也不是严格正确的。p[-1] = (int [1]){0}

H&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++;

然后代码将是正确的,并且会按照他们所说的去做:包含五个不同的指针,所有指针都指向其生命周期在循环中继续的 s,并且 、 等。p[0], ..., p[4]intprintf*(p[0]) == 0*(p[1]) == 1