Pergunta

#include <stdio.h>

int main(void)
{
   int i = 0;
   i = i++ + ++i;
   printf("%d\n", i); // 3

   i = 1;
   i = (i++);
   printf("%d\n", i); // 2 Should be 1, no ?

   volatile int u = 0;
   u = u++ + ++u;
   printf("%d\n", u); // 1

   u = 1;
   u = (u++);
   printf("%d\n", u); // 2 Should also be one, no ?

   register int v = 0;
   v = v++ + ++v;
   printf("%d\n", v); // 3 (Should be the same as u ?)

   int w = 0;
   printf("%d %d\n", ++w, w); // shouldn't this print 1 1

   int x[2] = { 5, 8 }, y = 0;
   x[y] = y ++;
   printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}
Foi útil?

Solução

C tem o conceito de comportamento indefinido, ou seja, algumas construções de linguagem são sintaticamente válido, mas você não pode prever o comportamento quando o código é executado.

Tanto quanto eu sei, o padrão não diz explicitamente por o conceito de comportamento indefinido existe. Na minha mente, é simplesmente porque os projetistas da linguagem queria que houvesse alguma margem de manobra na semântica, em vez de ie exigindo que todas as implementações lidar com integer overflow no exatamente da mesma maneira, que muito provavelmente imporia custos graves de desempenho, eles só deixaram o comportamento indefinido de modo que se você escrever código que causas integer overflow, tudo pode acontecer.

Então, com isso em mente, por que são esses "problemas"? A linguagem diz claramente que certas coisas levar a um comportamento indefinido . Não há nenhum problema, não há "deveria" envolvidos. Se o comportamento indefinido muda quando uma das variáveis ??envolvidas é declarado volatile, isso não prova ou mudar nada. É indefinido ; você não pode raciocinar sobre o comportamento.

A sua aparência interessante exemplo mais, aquele com

u = (u++);

é um exemplo clássico de comportamento indefinido (veja a entrada da Wikipedia sobre seqüência pontos ).

Outras dicas

Apenas compilar e desmontar sua linha de código, se você é tão inclinado para saber exatamente como é que você recebe o que você está recebendo.

Este é o que eu recebo na minha máquina, juntamente com o que eu acho que está acontecendo:

$ cat evil.c
void evil(){
  int i = 0;
  i+= i++ + ++i;
}
$ gcc evil.c -c -o evil.bin
$ gdb evil.bin
(gdb) disassemble evil
Dump of assembler code for function evil:
   0x00000000 <+0>:   push   %ebp
   0x00000001 <+1>:   mov    %esp,%ebp
   0x00000003 <+3>:   sub    $0x10,%esp
   0x00000006 <+6>:   movl   $0x0,-0x4(%ebp)  // i = 0   i = 0
   0x0000000d <+13>:  addl   $0x1,-0x4(%ebp)  // i++     i = 1
   0x00000011 <+17>:  mov    -0x4(%ebp),%eax  // j = i   i = 1  j = 1
   0x00000014 <+20>:  add    %eax,%eax        // j += j  i = 1  j = 2
   0x00000016 <+22>:  add    %eax,-0x4(%ebp)  // i += j  i = 3
   0x00000019 <+25>:  addl   $0x1,-0x4(%ebp)  // i++     i = 4
   0x0000001d <+29>:  leave  
   0x0000001e <+30>:  ret
End of assembler dump.

(I ... supor que a instrução 0x00000014 era algum tipo de otimização do compilador?)

Eu acho que as partes relevantes do padrão C99 são 6,5 Expressions, § 2

Entre o ponto sequência anterior e ao lado um objeto deve ter um valor armazenado modificado, no máximo, uma vez que através da avaliação de uma expressão. Além disso, o valor prévio devem ser lidas apenas para determinar o valor a ser armazenado.

e 6.5.16 Operadores de atribuição, §4:

A ordem de avaliação dos operandos não é especificado. Se for feita uma tentativa de modificar o resultado de um operador de atribuição ou para aceder a ele depois de o ponto seguinte sequência, a comportamento é indefinido.

O comportamento não pode realmente ser explicado porque invoca tanto comportamento não especificado e indefinido comportamento , por isso não podemos fazer previsões gerais sobre este código, mas se você ler Olve Maudal de trabalho, tais como profunda C e não especificado e Undefined às vezes você pode fazer bons palpites em casos muito específicos, com um compilador e ambiente específico, mas por favor não faça isso em qualquer lugar perto de produção.

Então, passar para comportamento não especificado , em projecto c99 parágrafo section6.5 padrão 3 diz ( grifo meu ):

O agrupamento dos operadores e dos operandos é indicado pela syntax.74) Excepto como especificado posterior (para a função de chamada (), &&, ||,:?, e os operadores por vírgula), a ordem de avaliação das subexpressões e a ordem em que os efeitos colaterais ocorrem são ambos não especificado <. / p>

Assim, quando temos uma linha como esta:

i = i++ + ++i;

não sabemos se i++ ou ++i será avaliada primeiro. Isto é principalmente para dar o compilador melhores opções para otimização .

Temos também comportamento indefinido aqui também desde que o programa está modificando variáveis ??(i, u, etc ..) mais de uma vez entre pontos de seqüência . Do projecto parágrafo seção 6.5 padrão 2 ( grifo meu ):

Entre o ponto sequência anterior e ao lado um objeto deve ter um valor armazenado modificada no máximo uma vez por meio da avaliação de uma expressão. Além disso, o valor anterior devem ser lidas apenas para determinar o valor a ser armazenado .

cita os seguintes exemplos de código como sendo indefinido:

i = ++i + 1;
a[i++] = i; 

Em todos estes exemplos, o código é a tentativa para modificar um objecto mais do que uma vez no mesmo ponto de sequência, que termina com o ; em cada um destes casos:

i = i++ + ++i;
^   ^       ^

i = (i++);
^    ^

u = u++ + ++u;
^   ^       ^

u = (u++);
^    ^

v = v++ + ++v;
^   ^       ^

comportamento não especificado é definido no projecto c99 padrão na seção 3.4.4 como:

uso de um valor não especificado, ou outro comportamento onde esta Norma fornece duas ou mais possibilidades e não impõe qualquer requisito em que é escolhido em qualquer instância

e comportamento indefinido é definida no ponto 3.4.3 como:

comportamento, mediante a utilização de uma construção programa não portável ou errónea ou de dados errados, para o qual esta Norma não impõe requisitos

e notas que:

possível comportamento intervalos indefinidos de ignorar completamente a situação com resultados imprevisíveis, para se comportar durante a tradução ou a execução do programa de uma forma característica documentada do ambiente (com ou sem a emissão de uma mensagem de diagnóstico), para terminar uma tradução ou de execução ( com a emissão de uma mensagem de diagnóstico).

A maioria das respostas aqui citado do padrão C enfatizando que o comportamento destas construções são indefinido. Para entender por que o comportamento dessas construções são indefinidos , vamos entender esses termos primeiros, à luz do padrão C11:

Sequenced: (5.1.2.3)

Dado qualquer duas avaliações A e B, se A é seqüenciado antes B, em seguida, a execução de A deve preceder a execução de B.

unsequenced:

Se A não é seqüenciado antes ou depois B, então A e B são unsequenced.

As avaliações podem ser uma de duas coisas:

  • computações valor , que trabalham fora o resultado de uma expressão; e
  • efeitos colaterais , que são modificações de objetos.

Sequence Point:

A presença de um ponto de sequência entre a avaliação das expressões A e B implica que cada valor computacional e efeito colateral associada com A é sequenciado antes de cada valor computação e efeito colateral associado com B.

Agora, chegando à pergunta, para as expressões como

int i = 1;
i = i++;

padrão diz que:

6,5 Expressões:

Se um efeito secundário de um objecto de escalar é unsequenced relativa para quer um efeito secundário diferente no mesmo objecto escalar ou um cálculo do valor com o valor da mesma escalar objeto, o comportamento é indefinido . [...]

Portanto, os invoca de expressão acima UB porque dois efeitos colaterais sobre o mesmo i objecto é unsequenced em relação ao outro. Isso significa que não é seqüenciado se o efeito lado a atribuição para i será feito antes ou após o efeito lado a ++.
Dependendo se a atribuição ocorre antes ou depois do incremento, diferentes resultados será produzido e que é a do caso de comportamento indefinido .

Vamos renomear o i à esquerda de cessão ser il e ao direito de atribuição (no i++ expressão) ser ir, então a expressão ser como

il = ir++     // Note that suffix l and r are used for the sake of clarity.
              // Both il and ir represents the same object.  

Um ponto importante sobre Postfix operador ++ é que:

apenas porque o ++ vem após a variável não significa que o incremento acontece tarde . O incremento pode acontecer tão cedo quanto o compilador gosta enquanto os garante compilador que o valor original é usado .

Isso significa que o il = ir++ expressão poderia ser avaliado, como

temp = ir;      // i = 1
ir = ir + 1;    // i = 2   side effect by ++ before assignment
il = temp;      // i = 1   result is 1  

ou

temp = ir;      // i = 1
il = temp;      // i = 1   side effect by assignment before ++
ir = ir + 1;    // i = 2   result is 2  

resultando em dois resultados 1 e 2 diferente que depende da sequência de efeitos colaterais por atribuição e ++ e, portanto, invoca UB.

Outra maneira de responder a esta, ao invés de se atolar em detalhes secretos de pontos de seqüência e um comportamento indefinido, é simplesmente perguntar, o que eles estão querendo dizer? Qual foi o programador tentando fazer?

O primeiro fragmento perguntado sobre, i = i++ + ++i, é muito claramente insano em meu livro. Ninguém jamais escrevê-lo em um programa real, não é óbvio que ele faz, não há nenhum algoritmo alguém concebível poderia ter sido tentando código que teria resultado nesta sequência artificial particular de operações. E uma vez que não é óbvio para você e para mim o que é suposto fazer, está tudo bem em meu livro se o compilador não consegue descobrir o que é suposto fazer, qualquer um.

O segundo fragmento, i = i++, é um pouco mais fácil de entender. Alguém está claramente tentando incrementar i, e atribuir a parte de trás resultado para i. Mas há algumas maneiras de fazer isso em C. A maneira mais simples de adicionar 1 a i, e atribuir a parte de trás resultado para i, é o mesmo em quase qualquer linguagem de programação:

i = i + 1

C, é claro, tem um atalho:

i++

Isto significa, "adicionar 1 a i, e atribuir a parte de trás resultado para i". Então, se nós construímos uma mistura dos dois, por escrito

i = i++

o que estamos realmente dizendo é "adicionar 1 a i, e atribuir a parte de trás resultado para i, e atribuir a parte de trás resultado para i". Estamos confusos, por isso não me incomoda muito, se o compilador fica confuso também.

Na realidade, a única vez que essas expressões loucas são escritos é quando as pessoas estão usando-os como exemplos artificiais de como ++ é suposto trabalho. E é claro que é importante entender como ++ funciona. Mas uma regra prática para a utilização ++ é: "Se não é óbvio que uma expressão usando ++ meios, não escrevê-lo."

Nós costumávamos passar horas incontáveis ??em expressões comp.lang.c discutir como estes e por que está indefinido. Duas das minhas respostas mais longas, que tentam realmente explicar por que, são arquivados na web:

Veja também questão 3.8 eo resto das perguntas em seção 3 do C FAQ lista .

Muitas vezes, esta questão está ligada como uma duplicata de questões relacionadas com código como

printf("%d %d\n", i, i++);

ou

printf("%d %d\n", ++i, i++);

ou semelhantes variantes.

Enquanto este é também comportamento indefinido como já foi dito, há diferenças sutis quando printf() está envolvido quando se compara a uma declaração como:

x = i++ + i++;

No seguinte declaração:

printf("%d %d\n", ++i, i++);

ordem de avaliação de argumentos em printf() é não especificado . Isso significa que, expressões i++ e ++i poderia ser avaliado em qualquer ordem. C11 padrão tem algumas descrições relevantes sobre isso:

Anexo J, comportamentos não especificados

A ordem na qual a função designador, argumentos e subexpressions dentro dos argumentos são avaliados em uma chamada de função (6.5.2.2).

3.4.4, o comportamento não especificado

O uso de um valor não especificado, ou outro comportamento onde esta Norma fornece duas ou mais possibilidades e impõe Sem outras exigências no que é escolhido em qualquer instância.

EXEMPLO Um exemplo de comportamento não especificada é a ordem na qual o são avaliados argumentos para uma função.

O comportamento não especificado em si não é um problema. Veja este exemplo:

printf("%d %d\n", ++x, y++);

Isso também tem comportamento não especificado , porque a ordem de avaliação de ++x e y++ não é especificado. Mas é comunicado perfeitamente legal e válido. Há não comportamento indefinido nesta declaração. Porque as modificações (++x e y++) são feitos para distinta objetos.

O que torna a seguinte declaração

printf("%d %d\n", ++i, i++);

como comportamento indefinido é o fato de que essas duas expressões modificar o mesma objeto i sem uma intervenção ponto sequência .


Outro detalhe é que o vírgula envolvido na chamada printf () é uma separador , e não o operador vírgula .

Esta é uma distinção importante porque o operador vírgula faz introduzir um ponto sequência entre a avaliação de seus operandos, o que torna o seguinte legal:

int i = 5;
int j;

j = (++i, i++);  // No undefined behaviour here because the comma operator 
                 // introduces a sequence point between '++i' and 'i++'

printf("i=%d j=%d\n",i, j); // prints: i=7 j=6

O operador vírgula avalia seus operandos da esquerda para a direita e os rendimentos apenas o valor da última operando. Assim, em j = (++i, i++);, incrementos ++i i para 6 e i++ rendimentos de valor antigo da i (6) que é atribuído a j. Então i se torna 7 devido à pós-incremento.

Portanto, se o vírgula na chamada de função eram para ser um operador vírgula seguida

printf("%d %d\n", ++i, i++);

não será um problema. Mas ele chama comportamento indefinido , pois o vírgula aqui é uma separador .


Para aqueles que são novos para comportamento indefinido se beneficiar da leitura o que cada C programador deve saber sobre Undefined Comportamento para entender o conceito e muitas outras variantes do undecomportamento multado em C.

Este post:. indefinido, não especificado e definido pela implementação comportamento também é relevante

Embora seja improvável que qualquer compiladores e processadores seria realmente fazê-lo, seria legal, sob o padrão C, para o compilador para implementar "i ++" com a sequência:

In a single operation, read `i` and lock it to prevent access until further notice
Compute (1+read_value)
In a single operation, unlock `i` and store the computed value

Enquanto eu não acho que nenhum processadores suportam o hardware para permitir tal coisa a ser feito de forma eficiente, pode-se facilmente imaginar situações em que tal comportamento seria tornar o código de multi-threaded mais fácil (por exemplo, seria garantir que, se dois segmentos tentam executar a seqüência acima, simultaneamente, i iria ficar incrementado por dois) e não é totalmente inconcebível que alguns processador futuro pode fornecer uma característica algo parecido.

Se o compilador fosse escrever i++ como indicado acima (legal sob o padrão) e foram para intercalar as instruções acima ao longo da avaliação da expressão geral (também legal), e se isso não acontecer a notificação de que um dos as outras instruções aconteceram acesso i, seria possível (e legal) para o compilador para gerar uma sequência de instruções que impasse. Para ter certeza, um compilador quase certamente detectar o problema no caso em que o mesmo i variável é usada em ambos os lugares, mas se uma rotina aceita referências a dois ponteiros p e q e usos (*p) e (*q) na expressão acima (em vez que usando i duas vezes) o compilador não seriam obrigados a reconhecer ou evitar o impasse que poderia ocorrer se o endereço do mesmo objeto foram passados ??para ambos p e q.

O padrão C diz que uma variável só deve ser atribuída no máximo uma vez entre dois pontos de seqüência. Um ponto e vírgula, por exemplo, é um ponto de sequência.
Assim, cada declaração do formulário:

i = i++;
i = i++ + ++i;

e assim por diante violar essa regra. A norma também diz que o comportamento é indefinido e não não especificado. Alguns compiladores não detectar estes e produzir algum resultado, mas isso não é por padrão.

No entanto, duas variáveis ??diferentes podem ser incrementada entre dois pontos de seqüência.

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

A descrição acima é um comum prática de codificação ao copiar / análise de strings.

Enquanto o sintaxe das expressões como a = a++ ou a++ + a++ é legal, o comportamento dessas construções é indefinido porque um < em> deve no padrão C não é obedecida. C99 6.5p2 :

  1. Entre o ponto sequência anterior e ao lado um objeto deve ter um valor armazenado modificado no máximo uma vez por meio da avaliação de uma expressão. [72] Além disso, o valor anterior deve ser lido somente para determinar o valor a ser armazenado [73]

Com nota 73 esclarecendo ainda que

  1. Este parágrafo torna expressões declaração indefinidos, como

    i = ++i + 1;
    a[i++] = i;
    

    permitindo

    i = i + 1;
    a[i] = i;
    

Os vários pontos de sequência são listados no Anexo C das C11 (e C99 ):

  1. O que se segue são os pontos sequência descrita no 5.1.2.3:

    • Entre as avaliações do designador de função e argumentos reais em uma chamada de função e a chamada real. (6.5.2.2).
    • Entre as avaliações dos primeiro e segundo operandos dos seguintes operadores: AND lógico && (6.5.13); OR lógico || (6.5.14); vírgula, (6.5.17).
    • Entre as avaliações do primeiro operando da condicional? :. Operador e qualquer dos segundo e terceiro operandos é avaliada (6.5.15)
    • A extremidade de um Declarador cheio: declarators (6.7.6);
    • Entre a avaliação de uma expressão plena e a próxima plena expressão a ser avaliada. O que se segue são expressões completas: um inicializador que não é parte de um literal composto (6.7.9); a expressão em uma indicação da expressão (6.8.3); a expressão controle de uma instrução de seleção (se ou switch) (6.8.4); a expressão controle de um tempo ou fazer declaração (6.8.5); cada um dos (opcional) expressões de uma instrução for (6.8.5.3); a expressão (opcional) em um comunicado de retorno (6.8.6.4).
    • Imediatamente antes de uma função de biblioteca retorna (7.1.4).
    • Após as acções associado a cada entrada / saída especificador de conversão função formatada (7.21.6, 7.29.2).
    • Imediatamente antes e imediatamente após cada chamada para uma função de comparação, e também entre qualquer chamada de uma função de comparação e qualquer movimento dos objectos passados ??como argumentos para essa chamada (7.22.5).

O texto da mesma parágrafo em C11 é :

  1. Se um efeito secundário de um objecto de escalar é unsequenced relação a qualquer um efeito secundário diferente no mesmo objecto escalar ou um cálculo do valor com o valor do mesmo objecto escalar, o comportamento é indefinido. Se houver várias ordenações admissíveis dos subexpress~oes de uma expressão, o comportamento é indefinido se um tal efeito colateral unsequenced ocorre em qualquer um dos orderings.84)

Você pode detectar tais erros em um programa, por exemplo, usando uma versão recente do GCC com -Wall e -Werror, e depois GCC serão definitivas recusar-se a compilar seu programa. O seguinte é a saída do gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005:

% gcc plusplus.c -Wall -Werror -pedantic
plusplus.c: In function ‘main’:
plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
    i = i++ + ++i;
    ~~^~~~~~~~~~~
plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
plusplus.c:10:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
    i = (i++);
    ~~^~~~~~~
plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
    u = u++ + ++u;
    ~~^~~~~~~~~~~
plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
plusplus.c:18:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
    u = (u++);
    ~~^~~~~~~
plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
    v = v++ + ++v;
    ~~^~~~~~~~~~~
plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
cc1: all warnings being treated as errors

A parte importante é saber o que é um ponto de seqüência é - e o que é um ponto sequência e que não é . Por exemplo, o operador vírgula é um ponto de sequência, de modo

j = (i ++, ++ i);

é bem definida, e irão incrementar i por um, originando o valor antigo, que descarte valor; em seguida, a vírgula operador, para resolver os efeitos laterais; e depois i incremento por um, e o valor resultante torna-se o valor da expressão - ou seja, esta é apenas uma maneira artificial para escrever j = (i += 2) que ainda é novamente uma forma "inteligente" para escrever

i += 2;
j = i;

No entanto, o , em listas de argumentos de função é não um operador de vírgula, e não há nenhum ponto de seqüência entre as avaliações de argumentos distintos; em vez suas avaliações são unsequenced em relação uns aos outros; então a chamada de função

int i = 0;
printf("%d %d\n", i++, ++i, i);

tem comportamento indefinido porque não existe qualquer ponto sequência entre as avaliações de i++ e ++i em argumentos da função , e o valor de i é, por conseguinte, modificada por duas vezes, por tanto i++ e ++i, entre o anterior e o seguinte ponto de sequência.

Na https://stackoverflow.com/questions/29505280/incrementing-array-index- in-c alguém perguntou sobre uma declaração como:

int k[] = {0,1,2,3,4,5,6,7,8,9,10};
int i = 0;
int num;
num = k[++i+k[++i]] + k[++i];
printf("%d", num);

que imprime 7 ... o OP espera-para imprimir 6.

Os incrementos ++i não são garantidos para todos completos antes do resto dos cálculos. Na verdade, diferentes compiladores irá obter resultados diferentes aqui. No exemplo que você forneceu, o primeiro 2 ++i executado, em seguida, os valores de k[] foi lida, então o último ++i então k[].

num = k[i+1]+k[i+2] + k[i+3];
i += 3

compiladores modernos irá otimizar isso muito bem. Na verdade, possivelmente melhor do que o código que você escreveu originalmente (supondo que ele tinha trabalhado da maneira que você esperava).

A sua questão provavelmente não era: "Por que essas construções comportamento em C indefinidos?". Sua pergunta foi, provavelmente, "Por que esse código (usando ++) não me dar o valor que eu esperava?", E alguém marcou a sua pergunta como um duplicado, e enviou-lhe aqui.

O tentativas de resposta para responder a essa pergunta: por que seu código não lhe dar a resposta que você esperava, e como você pode aprender a reconhecer (e evitar) expressões que não funcionará como esperado <. / p>

Eu suponho que você já ouviu a definição básica de operadores ++ e -- de C por agora, e como o formulário de prefixo ++x difere do x++ forma postfix. Mas estes operadores são difíceis de pensar, de modo a certificar-se de que você entendeu, talvez você escreveu um pequeno programa de teste pouco envolvendo algo como

int x = 5;
printf("%d %d %d\n", x, ++x, x++);

Mas, para sua surpresa, este programa fez não o ajuda a entender - é impressa uma estranha saída, inesperada, inexplicável, sugerindo que talvez ++ faz algo completamente diferente, não em tudo o que você pensou ele fez.

Ou, talvez você está olhando para uma expressão difícil de entender como

int x = 5;
x = x++ + ++x;
printf("%d\n", x);

Talvez alguém lhe deu esse código como um quebra-cabeça. Este código também não faz sentido, especialmente se você executá-lo - e se você compilar e executá-lo em dois compiladores diferentes, é provável que você obter duas respostas diferentes! O que há com isso? Que resposta está correta? (E a resposta é que ambos são, ou nenhum deles é.)

Como você já ouviu até agora, todas essas expressões são indefinido , o que significa que a linguagem C não faz garantia sobre o que eles vão fazer. Este é um resultado estranho e surpreendente, porque você provavelmente pensou que qualquer programa que você poderia escrever, contanto que compilado e correu, geraria um único, de saída bem definido. Mas no caso de um comportamento indefinido, que não é assim.

O que faz uma expressão indefinida? São expressões que envolvem ++ e -- sempre indefinido? Claro que não:. Estas são operadores úteis, e se você usá-los corretamente, eles são perfeitamente bem definida

Para as expressões das quais estamos falando o que os torna indefinido é quando há muita coisa acontecendo ao mesmo tempo, quando não temos certeza que ordem as coisas vão acontecer, mas quando os assuntos a fim do resultado que temos.

Vamos voltar ir para os dois exemplos que usei nesta resposta. Quando eu escrevi

printf("%d %d %d\n", x, ++x, x++);

a questão é, antes de chamar printf, o compilador calcular o valor de x primeiro lugar, ou x++, ou talvez ++x? Mas acontece que não sabemos . Não há nenhuma regra em C, que diz que os argumentos para uma função de obter avaliados da esquerda para a direita ou para a direita da esquerda para a, ou de alguma outra forma. Portanto, não podemos dizer se o compilador vai fazer x primeiro, depois ++x, então x++, ou x++ então ++x então x, ou algum outro fim. Mas a ordem importa claramente, porque dependendo de qual ordem os usos do compilador, vamos claramente obter resultados diferentes impresso pelo printf.

E sobre essa expressão louco?

x = x++ + ++x;

O problema com essa expressão é que ele contém três diferentes tentativas para modificar o valor de x: (1) as tentativas parte x++ para adicionar 1 a x, armazenar o novo valor no x, e devolver o valor antigo de x; (2) a parte ++x tenta adicionar 1 a x, armazenar o novo valor no x, e retornar o novo valor de x; e (3) a parte x = tenta atribuir a soma dos outros dois para trás para x. Qual dessas três tentativas de atribuições vai "ganhar"? Qual dos três valores vai realmente começar assigned para x? Mais uma vez, e talvez surpreendentemente, não há nenhuma regra em C para nos dizer.

Você pode imaginar que a precedência ou associatividade ou avaliação da esquerda para a direita diz-lhe que ordem as coisas acontecem, mas eles não. Você pode não acreditar, mas por favor, tome minha palavra para ela, e eu vou dizer de novo: precedência e associatividade não determinam todos os aspectos da ordem de avaliação de uma expressão em C. Em particular, se dentro de uma expressão existem múltiplos diferentes pontos onde tentamos atribuir um novo valor para algo como x, precedência e associatividade fazer não diga-nos qual dessas tentativas ocorrer primeiro, ou último, nem nada.


Assim, com tudo o que de fundo e introdução fora do caminho, se você quiser ter certeza de que todos os seus programas são bem definidos, que expressões que você pode escrever, e que você não pode escrever?

Estas expressões são todos muito bem:

y = x++;
z = x++ + y++;
x = x + 1;
x = a[i++];
x = a[i++] + b[j++];
x[i++] = a[j++] + b[k++];
x = *p++;
x = *p++ + *q++;

Estas expressões são indefinidos:

x = x++;
x = x++ + ++x;
y = x + x++;
a[i] = i++;
a[i++] = i;
printf("%d %d %d\n", x, ++x, x++);

E a última pergunta é, como você pode dizer que as expressões são bem definido e que as expressões são indefinido?

Como eu disse anteriormente, as expressões indefinidos são aqueles onde há muita coisa acontecendo ao mesmo tempo, onde você não pode ter certeza que ordem as coisas acontecem, e onde as questões de ordem:

  1. Se há uma variável que está sendo modificado (atribuído a) em duas ou mais diferentes lugares, como você sabe que a modificação acontece pela primeira vez?
  2. Se há uma variável que está sendo modificado em um só lugar, e tendo o seu valor usado em outro lugar, como você sabe se ele usa o valor antigo ou o novo valor?

Como um exemplo de como 1, na expressão

x = x++ + ++x;

existem três tentativas de modificar `x.

Como um exemplo de # 2, na expressão

y = x + x++;

que ambos usam o valor da x, e modificá-lo.

Então, essa é a resposta: Certifique-se que, em qualquer expressão que você escreve, cada variável é modificado no máximo uma vez, e se uma variável é modificado, você também não tentar usar o valor dessa variável em outro lugar.

Uma boa explicação sobre o que acontece neste tipo de computação é fornecido no documento n1188 de site da ISO W14 .

Eu explicar as ideias.

A principal regra do padrão ISO 9899 que se aplica nesta situação é 6.5p2.

Entre o ponto sequência anterior e ao lado um objeto deve ter um valor armazenado modificado no máximo uma vez por meio da avaliação de uma expressão. Além disso, o valor anterior deve ser lido somente para determinar o valor a ser armazenado.

Os pontos de sequência em uma expressão como i=i++ são antes e depois i= i++.

No artigo que citei acima é explicado que você pode descobrir o programa como sendo formada por pequenas caixas, cada caixa que contém as instruções entre 2 pontos de seqüência consecutivos. Os pontos de sequência são definidos no anexo C da norma, em caso de i=i++ existem 2 pontos de sequência que delimitam um-expressão completa. Tal expressão é sintacticamente equivalente com uma entrada de expression-statement sob a forma de Backus-Naur da gramática (uma gramática é fornecido no Anexo A do padrão).

Assim, a ordem de instruções dentro de uma caixa não tem nenhuma ordem clara.

i=i++

pode ser interpretado como

tmp = i
i=i+1
i = tmp

ou como

tmp = i
i = tmp
i=i+1

porque ambas as todas estas formas de interpretar a i=i++ código são válidos e porque ambos geram respostas diferentes, o comportamento é indefinido.

Portanto, um ponto de sequência pode ser visto no início e no final de cada caixa que compõe o programa [as caixas são unidades atómicas em C] e dentro de uma caixa a fim das instruções não é definido em todos os casos. Alterar que uma ordem pode mudar o resultado às vezes.

EDIT:

Outra fonte boa para explicar tais ambiguidades são as entradas de c-faq local (também publicado como um livro ), ou seja, aqui e aqui aqui .

A razão é que o programa está sendo executado um comportamento indefinido. As mentiras problema na ordem de avaliação, porque não há nenhum pontos sequência necessária de acordo com a C ++ 98 padrão (sem operações é sequenciado antes ou após o outro de acordo com a terminologia C ++ 11).

No entanto, se você ficar com um compilador, você vai encontrar o comportamento persistente, contanto que você não adicionar chamadas de função ou ponteiros, o que tornaria o comportamento mais confuso.

  • Então, primeiro o GCC: Usando Nuwen MinGW 15 GCC 7.1 você irá obter:

    #include<stdio.h>
    int main(int argc, char ** argv)
    {
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 2
    
    i = 1;
    i = (i++);
    printf("%d\n", i); //1
    
    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 2
    
    u = 1;
    u = (u++);
    printf("%d\n", u); //1
    
    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); //2
    

    }

Como funciona o GCC? avalia sub expressões em uma esquerda para a direita para o lado da mão direita (RHS), em seguida, atribui o valor para o lado esquerdo (LHS). Isto é exatamente como Java e C # se comportam e definir seus padrões. (Sim, o software equivalente em Java e C # definiu comportamentos). É avaliar cada uma sub expressão por um na Declaração de RHS em uma esquerda para a direita; para cada sub expressão:. a c ++ (pré-incremento) é avaliada em primeiro lugar, em seguida, o valor de c é utilizado para a operação, em seguida, o pós incremento c ++)

GCC C ++: Operadores

GCC C ++, a precedência dos controlos operadores a ordem em que os operadores individuais são avaliadas

o código equivalente em comportamento C ++ definido como CCG compreende:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    //i = i++ + ++i;
    int r;
    r=i;
    i++;
    ++i;
    r+=i;
    i=r;
    printf("%d\n", i); // 2

    i = 1;
    //i = (i++);
    r=i;
    i++;
    i=r;
    printf("%d\n", i); // 1

    volatile int u = 0;
    //u = u++ + ++u;
    r=u;
    u++;
    ++u;
    r+=u;
    u=r;
    printf("%d\n", u); // 2

    u = 1;
    //u = (u++);
    r=u;
    u++;
    u=r;
    printf("%d\n", u); // 1

    register int v = 0;
    //v = v++ + ++v;
    r=v;
    v++;
    ++v;
    r+=v;
    v=r;
    printf("%d\n", v); //2
}

Em seguida, vamos para Visual Studio . Visual Studio 2015, você obtém:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 3

    i = 1;
    i = (i++);
    printf("%d\n", i); // 2 

    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 3

    u = 1;
    u = (u++);
    printf("%d\n", u); // 2 

    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); // 3 
}

Como funciona o estúdio visual, é preciso uma outra abordagem, que avalia todos os pré-incrementos expressões na primeira passagem, em seguida, utiliza valores variáveis ??nas operações na segunda passagem, atribuir a partir de RHS para LHS na terceira passagem, em seguida, na última passagem ele avalia todas as expressões pós-incremento em uma passagem.

Então, o equivalente no comportamento C ++ definido como Visual C ++ entende:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int r;
    int i = 0;
    //i = i++ + ++i;
    ++i;
    r = i + i;
    i = r;
    i++;
    printf("%d\n", i); // 3

    i = 1;
    //i = (i++);
    r = i;
    i = r;
    i++;
    printf("%d\n", i); // 2 

    volatile int u = 0;
    //u = u++ + ++u;
    ++u;
    r = u + u;
    u = r;
    u++;
    printf("%d\n", u); // 3

    u = 1;
    //u = (u++);
    r = u;
    u = r;
    u++;
    printf("%d\n", u); // 2 

    register int v = 0;
    //v = v++ + ++v;
    ++v;
    r = v + v;
    v = r;
    v++;
    printf("%d\n", v); // 3 
}

como os estados documentação do Visual Studio em Precedência e ordem de avaliação :

Sempre que vários operadores aparecem juntos, eles têm a mesma precedência e são avaliados de acordo com sua associatividade. Os operadores na tabela são descritos nas seções que começam com operadores Postfix.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top