#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?
}
815
(i++)
ainda avalia a 1, independentemente de parêntesesi = (i++);
se pretendia fazer, certamente existe uma maneira mais clara de escrevê-lo. Isso seria verdade mesmo se estivesse bem definido. Mesmo em Java, que define o comportamento dei = (i++);
, ainda é um código incorreto. Basta escrever #i++;
Respostas:
C tem o conceito de comportamento indefinido, ou seja, algumas construções de linguagem são sintaticamente válidas, mas você não pode prever o comportamento quando o código é executado.
Até onde eu sei, o padrão não diz explicitamente por que o conceito de comportamento indefinido existe. Na minha opinião, é simplesmente porque os projetistas de linguagem queriam que houvesse alguma margem de manobra na semântica, em vez de exigir que todas as implementações lidassem com excesso de números inteiros da mesma maneira exata, o que provavelmente imporia custos sérios de desempenho, eles apenas deixaram o comportamento indefinido para que, se você escrever um código que cause excesso de número inteiro, tudo possa acontecer.
Então, com isso em mente, por que esses "problemas"? A linguagem diz claramente que certas coisas levam a um comportamento indefinido . Não há problema, não há "deveria" envolvido. Se o comportamento indefinido for alterado quando uma das variáveis envolvidas for declarada
volatile
, isso não prova ou altera nada. Está indefinido ; você não pode raciocinar sobre o comportamento.Seu exemplo mais interessante, aquele com
é um exemplo de comportamento indefinido em livro-texto (consulte a entrada da Wikipedia sobre pontos de sequência ).
fonte
i = ++i + 1;
.Basta compilar e desmontar sua linha de código, se você estiver tão inclinado a saber exatamente como é obter o que está recebendo.
É isso que recebo na minha máquina, junto com o que acho que está acontecendo:
(... suponho que a instrução 0x00000014 fosse algum tipo de otimização do compilador?)
fonte
gcc evil.c -c -o evil.bin
egdb evil.bin
→disassemble evil
, ou o que os equivalentes do Windows daqueles são :)Why are these constructs undefined behavior?
.gcc -S evil.c
), que é tudo o que é necessário aqui. Montar e desmontar é apenas uma maneira indireta de fazê-lo.Eu acho que as partes relevantes do padrão C99 são 6.5 Expressions, §2
e 6.5.16 Operadores de atribuição, §4:
fonte
i=i=5
é também um comportamento indefinidoA=B=5;
"Bloqueio de gravação A; Bloqueio de gravação B; Armazene 5 a A; loja 5 a B; desbloqueie B ; Desbloqueie A; "e uma instrução comoC=A+B;
" Bloqueio de leitura A; Bloqueio de leitura B; Cálculo A + B; Desbloqueie A e B; Bloqueio de gravação C; Armazene o resultado; Desbloqueie C; ". Isso garantiria que, se um encadeamento ocorresseA=B=5;
enquanto outro,C=A+B;
o último encadeamento veria ambas as gravações como tendo ocorrido ou nenhuma. Potencialmente uma garantia útil. Se um thread fezI=I=5;
, no entanto, ... #A maioria das respostas aqui citadas do padrão C enfatiza que o comportamento dessas construções é indefinido. Para entender por que o comportamento dessas construções não é definido , vamos entender esses termos primeiro à luz do padrão C11:
Sequenciado: (5.1.2.3)
Sem seqüência:
As avaliações podem ser uma de duas coisas:
Ponto de sequência:
Agora voltando à pergunta, para as expressões como
padrão diz que:
6.5 Expressões:
Portanto, a expressão acima invoca UB porque dois efeitos colaterais no mesmo objeto
i
são sem seqüência em relação um ao outro. Isso significa que não é sequenciado se o efeito colateral por atribuiçãoi
será realizado antes ou depois do efeito colateral por++
.Dependendo da atribuição ocorrer antes ou depois do incremento, resultados diferentes serão produzidos e esse é o caso de comportamento indefinido .
Vamos renomear a
i
esquerda da atribuição beil
e à direita da atribuição (na expressãoi++
) beir
, então a expressão será comoUm ponto importante em relação ao
++
operador Postfix é que:Isso significa que a expressão
il = ir++
pode ser avaliada comoou
resultando em dois resultados diferentes
1
e2
que depende da sequência de efeitos colaterais por atribuição++
e, portanto, chama UB.fonte
O comportamento não pode realmente ser explicado porque invoca o comportamento não especificado quanto o indefinido ; portanto, não podemos fazer previsões gerais sobre esse código, embora se você ler o trabalho de Olve Maudal , como Deep C e Unespecified and Undefined, às vezes você poderá obter bons resultados. palpites em casos muito específicos com um compilador e um ambiente específicos, mas não faça isso nem perto da produção.
Então, passando para um comportamento não especificado , no rascunho da seção padrão da c99 , o
6.5
parágrafo 3 diz ( ênfase minha ):Então, quando temos uma linha como esta:
não sabemos se serão
i++
ou++i
não avaliados primeiro. Isso é principalmente para fornecer ao compilador melhores opções de otimização .Nós também têm um comportamento indefinido aqui também desde que o programa está a modificar variáveis (
i
,u
, etc ..) mais de uma vez entre os pontos de sequência . Do6.5
parágrafo de seção padrão de desenho 2 ( ênfase minha ):cita os seguintes exemplos de código como indefinidos:
Em todos esses exemplos, o código está tentando modificar um objeto mais de uma vez no mesmo ponto de sequência, que terminará com o
;
em cada um desses casos:O comportamento não especificado é definido no rascunho da norma c99 na seção
3.4.4
como:O comportamento indefinido é definido na seção
3.4.3
como:e observa que:
fonte
Outra maneira de responder a isso, em vez de se atolar em detalhes misteriosos de pontos de sequência e comportamento indefinido, é simplesmente perguntar: o que eles deveriam significar? O que o programador estava tentando fazer?
O primeiro fragmento perguntado sobre,
i = i++ + ++i
é claramente insano no meu livro. Ninguém jamais escreveria isso em um programa real, não é óbvio o que ele faz, não há algoritmo concebível que alguém possa estar tentando codificar que resultaria nessa sequência artificial de operações. E como não é óbvio para você e para mim o que deve fazer, tudo bem no meu livro se o compilador também não puder descobrir o que deve fazer.O segundo fragmento,,
i = i++
é um pouco mais fácil de entender. Alguém está claramente tentando incrementar i e atribuir o resultado de volta a i. Mas existem algumas maneiras de fazer isso em C. A maneira mais básica de adicionar 1 a i e atribuir o resultado de volta a i é a mesma em quase qualquer linguagem de programação:C, é claro, tem um atalho útil:
Isso significa "adicione 1 a i e atribua o resultado de volta a i". Portanto, se construirmos uma mistura de dois, escrevendo
o que realmente estamos dizendo é "adicione 1 a i e atribua o resultado de volta a i, e atribua o resultado de volta a i". Estamos confusos, por isso não me incomoda muito se o compilador ficar confuso também.
Realisticamente, a única vez em que essas expressões malucas são escritas é quando as pessoas as usam como exemplos artificiais de como o ++ deve funcionar. E, claro, é importante entender como o ++ funciona. Mas uma regra prática para usar ++ é: "Se não for óbvio o que significa uma expressão usando ++, não a escreva".
Costumávamos passar inúmeras horas no comp.lang.c discutindo expressões como essas e por que elas são indefinidas. Duas das minhas respostas mais longas, que tentam realmente explicar o porquê, estão arquivadas na Web:
Veja também questionar 3.8 eo resto das perguntas em seção 3 da lista C FAQ .
fonte
*p=(*q)++;
,if (p!=q) *p=(*q)++; else *p= __ARBITRARY_VALUE;
isso significa que esse não é mais o caso. C hiper-moderno exigiria escrever algo como a última formulação (embora não exista uma maneira padrão de indicar que o código não se importa*p
) para atingir o nível de compiladores de eficiência usados para fornecer ao primeiro (aelse
cláusula é necessária para permitir o compilador otimiza oif
que alguns compiladores mais recentes exigiriam).assert
declarações, para que o programador possa preceder a linha em questão com um simplesassert(p != q)
. (É claro, fazer esse curso também exigiria reescrever<assert.h>
para não excluir as afirmações diretamente nas versões sem depuração, mas transformá-las em algo__builtin_assert_disabled()
que o compilador possa ver e depois não emitir código para.)Muitas vezes, essa pergunta é vinculada como uma duplicata de questões relacionadas a códigos como
ou
ou variantes semelhantes.
Embora esse também seja um comportamento indefinido, como já declarado, há diferenças sutis quando
printf()
envolvido ao comparar com uma declaração como:Na declaração a seguir:
a ordem de avaliação dos argumentos não
printf()
é especificada . Isso significa expressõesi++
e++i
pode ser avaliado em qualquer ordem. O padrão C11 tem algumas descrições relevantes sobre isso:Anexo J, comportamentos não especificados
3.4.4, comportamento não especificado
O comportamento não especificado em si NÃO é um problema. Considere este exemplo:
Isso também tem comportamento não especificado porque a ordem de avaliação
++x
ey++
não é especificada. Mas é uma declaração perfeitamente legal e válida. Não há comportamento indefinido nesta declaração. Porque as modificações (++x
ey++
) são feitas em objetos distintos .O que torna a seguinte declaração
como comportamento indefinido é o fato de que essas duas expressões modificam o mesmo objeto
i
sem um ponto de sequência intermediário .Outro detalhe é que a vírgula envolvida na chamada printf () é um separador , não o operador de vírgula .
Essa é uma distinção importante porque o operador de vírgula introduz um ponto de sequência entre a avaliação de seus operandos, o que torna legal o seguinte:
O operador de vírgula avalia seus operandos da esquerda para a direita e produz apenas o valor do último operando. Assim, em
j = (++i, i++);
,++i
incrementosi
para6
ei++
rendimentos de valor antigo dai
(6
) que é atribuído aj
. Em seguida,i
torna-se7
devido ao pós-incremento.Portanto, se a vírgula na chamada de função fosse um operador de vírgula,
não será um problema. Mas ele invoca um comportamento indefinido porque a vírgula aqui é um separador .
Para quem é novo no comportamento indefinido, se beneficiaria da leitura do que todo programador C deve saber sobre o comportamento indefinido para entender o conceito e muitas outras variantes de comportamento indefinido em C.
Esta publicação: Comportamento indefinido, não especificado e definido pela implementação também é relevante.
fonte
int a = 10, b = 20, c = 30; printf("a=%d b=%d c=%d\n", (a = a + b + c), (b = b + b), (c = c + c));
parece fornecer um comportamento estável (avaliação do argumento da direita para a esquerda no gcc v7.3.0; resultado "a = 110 b = 40 c = 60"). É porque as atribuições são consideradas como 'declarações completas' e, assim, introduzem um ponto de sequência? Isso não deveria resultar na avaliação de argumentos / declarações da esquerda para a direita? Ou é apenas manifestação de comportamento indefinido?b
ec
no terceiro e quarto argumentos, respectivamente, e lendo no segundo argumento. Mas não há sequência entre essas expressões (segundo, terceiro e quarto argumentos). O gcc / clang tem uma opção-Wsequence-point
que pode ajudar a encontrá-los também.Embora seja improvável que qualquer compilador e processador o faça, seria legal, sob o padrão C, que o compilador implementasse "i ++" com a sequência:
Embora eu ache que nenhum processador suporte o hardware para permitir que isso seja feito com eficiência, é fácil imaginar situações em que esse comportamento facilitaria o código multiencadeado (por exemplo, garantiria que, se dois threads tentassem executar o procedimento acima). sequência simultaneamente,
i
seria incrementado em dois) e não é totalmente inconcebível que algum processador futuro forneça um recurso parecido com esse.Se o compilador escrever
i++
como indicado acima (legal sob o padrão) e intercalar as instruções acima em toda a avaliação da expressão geral (também legal), e se não perceber que uma das outras instruções ocorreu para acessari
, seria possível (e legal) para o compilador gerar uma sequência de instruções que causariam um conflito. Certamente, um compilador quase certamente detectaria o problema no caso em que a mesma variáveli
é usada nos dois lugares, mas se uma rotina aceitar referências a dois ponteirosp
eq
, and use(*p)
e(*q)
na expressão acima (em vez de usari
duas vezes) não seria necessário que o compilador reconhecesse ou evitasse o conflito que ocorreria se o endereço do mesmo objeto fosse passado para ambos .p
eq
fonte
Embora a sintaxe das expressões como
a = a++
oua++ + a++
seja legal, o comportamento dessas construções seja indefinido porque uma regra no padrão C não é obedecida. C99 6.5p2 :Com a nota 73, esclarecendo ainda mais que
Os vários pontos de sequência estão listados no anexo C de C11 (e C99 ):
A redação do mesmo parágrafo em C11 é:
Você pode detectar esses erros em um programa, por exemplo, usando uma versão recente do GCC com
-Wall
e-Werror
, em seguida, o GCC se recusará totalmente a compilar seu programa. A seguir está a saída do gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005:A parte importante é saber o que é um ponto de sequência - e o que é um ponto de sequência e o que não é . Por exemplo, o operador de vírgula é um ponto de sequência, portanto
é bem definido e será incrementado
i
em um, produzindo o valor antigo, descarte esse valor; depois, no operador de vírgula, decida os efeitos colaterais; e então incrementai
em um, e o valor resultante se torna o valor da expressão - isto é, apenas uma maneira artificial de escrever,j = (i += 2)
que é novamente uma maneira "inteligente" de escreverNo entanto, as
,
listas de argumentos da função in não são um operador de vírgula e não há ponto de sequência entre avaliações de argumentos distintos; em vez disso, suas avaliações não são sequenciais uma em relação à outra; então a chamada de funçãopossui comportamento indefinido porque não há ponto de sequência entre as avaliações dos argumentos
i++
e++i
nos argumentos das funções , e o valor dei
é, portanto, modificado duas vezes, por ambosi++
e++i
entre o ponto de sequência anterior e o próximo.fonte
O padrão C diz que uma variável só deve ser atribuída no máximo uma vez entre dois pontos de sequência. Um ponto e vírgula, por exemplo, é um ponto de sequência.
Portanto, toda declaração do formulário:
e assim por diante violam essa regra. O padrão também diz que o comportamento é indefinido e não especificado. Alguns compiladores os detectam e produzem algum resultado, mas isso não é por padrão.
No entanto, duas variáveis diferentes podem ser incrementadas entre dois pontos de sequência.
A descrição acima é uma prática comum de codificação ao copiar / analisar seqüências de caracteres.
fonte
Em /programming/29505280/incrementing-array-index-in-c alguém perguntou sobre uma declaração como:
que imprime 7 ... o OP esperava imprimir 6.
Não
++i
é garantido que todos os incrementos sejam concluídos antes do restante dos cálculos. De fato, diferentes compiladores obterão resultados diferentes aqui. No exemplo que você forneceu, os dois primeiros++i
executados, os valores dek[]
foram lidos e os últimos++i
entãok[]
.Compiladores modernos otimizarão isso muito bem. De fato, possivelmente melhor do que o código que você escreveu originalmente (assumindo que funcionou da maneira que você esperava).
fonte
Uma boa explicação sobre o que acontece neste tipo de computação é fornecido no documento n1188 do site da ISO W14 .
Eu explico as idéias.
A regra principal da norma ISO 9899 que se aplica nessa situação é 6.5p2.
Os pontos de sequência em uma expressão como
i=i++
são antesi=
e depoisi++
.No artigo que citei acima, é explicado que você pode descobrir o programa como sendo formado por pequenas caixas, cada uma contendo as instruções entre 2 pontos de sequência consecutivos. Os pontos de sequência são definidos no anexo C da norma, no caso de
i=i++
existirem 2 pontos de sequência que delimitam uma expressão completa. Tal expressão é sintaticamente equivalente a uma entradaexpression-statement
na forma Backus-Naur da gramática (uma gramática é fornecida no anexo A da Norma).Portanto, a ordem das instruções dentro de uma caixa não tem uma ordem clara.
pode ser interpretado como
ou como
porque todas essas formas de interpretar o código
i=i++
são válidas e porque ambas 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 ordem das instruções não é definida em todos os casos. Mudando essa ordem, pode-se alterar o resultado algumas vezes.
EDITAR:
Outra boa fonte para explicar essas ambiguidades são as entradas do site c-faq (também publicadas como livro ), aqui e aqui e aqui .
fonte
i=i++
são muito semelhantes a esta resposta .Sua pergunta provavelmente não foi "Por que essas construções são um comportamento indefinido em C?". Sua pergunta foi provavelmente: "Por que esse código (usando
++
) não me deu o valor que eu esperava?", E alguém marcou sua pergunta como duplicada e o enviou aqui.Esta resposta tenta responder a essa pergunta: por que seu código não deu a resposta que você esperava e como você pode aprender a reconhecer (e evitar) expressões que não funcionarão conforme o esperado.
Suponho que você tenha ouvido a definição básica de C
++
e--
operadores e como o formulário de prefixo++x
difere do formulário de postfixx++
. Mas é difícil pensar nesses operadores; portanto, para ter certeza de que você entendeu, talvez você tenha escrito um pequeno programa de teste envolvendo algo comoMas, para sua surpresa, este programa não o ajudou a entender - ele imprimiu uma saída estranha, inesperada e inexplicável, sugerindo que talvez
++
faça algo completamente diferente, nem um pouco o que você pensou que ele fez.Ou talvez você esteja vendo uma expressão difícil de entender como
Talvez alguém tenha lhe dado esse código como um quebra-cabeça. Esse código também não faz sentido, especialmente se você o executa - e se você o compilar e executar em dois compiladores diferentes, é provável que você obtenha duas respostas diferentes! O que há com isso? Qual resposta está correta? (E a resposta é que ambos são, ou nenhum deles é.)
Como você já ouviu falar agora, todas essas expressões são indefinidas , o que significa que a linguagem C não garante o que fará. Esse é um resultado estranho e surpreendente, porque você provavelmente pensou que qualquer programa que pudesse escrever, contanto que fosse compilado e executado, geraria uma saída única e bem definida. Mas no caso de comportamento indefinido, não é assim.
O que torna uma expressão indefinida? As expressões que envolvem
++
e--
sempre indefinidas? Claro que não: esses são operadores úteis e, se você os usar corretamente, serão perfeitamente bem definidos.Para as expressões em que estamos falando, o que as torna indefinidas é quando há muita coisa acontecendo ao mesmo tempo, quando não sabemos ao certo em que ordem as coisas acontecerão, mas quando a ordem importa para o resultado que obtemos.
Vamos voltar aos dois exemplos que usei nesta resposta. Quando eu escrevi
a questão é, antes de chamar
printf
, o compilador calcula o valor dex
first, oux++
, ou talvez++x
? Mas acontece que não sabemos . Não existe uma regra em C que diga que os argumentos para uma função sejam avaliados da esquerda para a direita ou da direita para a esquerda ou em alguma outra ordem. Portanto, não podemos dizer se o compilador vai fazerx
em primeiro lugar, então++x
, em seguidax++
, oux++
então++x
, em seguidax
, ou algum outro fim. Mas a ordem é claramente importante, porque, dependendo de qual ordem o compilador usa, obteremos claramente diferentes resultados impressosprintf
.E essa expressão maluca?
O problema com esta expressão é que ela contém três tentativas diferentes para modificar o valor de x: (1) a
x++
parte tenta adicionar 1 a x, armazenar o novo valor emx
e retornar o valor antigo dex
; (2) a++x
peça tenta adicionar 1 a x, armazenar o novo valor emx
e retornar o novo valor dex
; e (3) ax =
parte tenta atribuir a soma dos outros dois de volta a x. Qual dessas três tentativas de atribuição "vencerá"? A qual dos três valores será atribuídox
? Novamente, e talvez surpreendentemente, não há regra em C para nos dizer.Você pode imaginar que a precedência ou associatividade ou a avaliação da esquerda para a direita informa em que ordem as coisas acontecem, mas elas não acontecem. Você pode não acreditar em mim, mas aceite minha palavra e digo novamente: precedência e associatividade não determinam todos os aspectos da ordem de avaliação de uma expressão em C. Em particular, se em uma expressão houver várias pontos diferentes em que tentamos atribuir um novo valor a algo como
x
precedência e associatividade não nos dizem qual dessas tentativas acontece primeiro, ou último, ou algo assim.Portanto, com todo esse histórico e introdução fora do caminho, se você quiser garantir que todos os seus programas estejam bem definidos, quais expressões você pode escrever e quais você não pode escrever?
Essas expressões são boas:
Essas expressões são todas indefinidas:
E a última pergunta é: como você pode dizer quais expressões estão bem definidas e quais são indefinidas?
Como eu disse anteriormente, as expressões indefinidas são aquelas em que há muita coisa ao mesmo tempo, onde você não pode ter certeza de qual ordem as coisas acontecem e onde a ordem importa:
Como um exemplo de # 1, na expressão
existem três tentativas para modificar o `x.
Como um exemplo de # 2, na expressão
nós dois usamos o valor de
x
e o modificamos.Portanto, essa é a resposta: verifique se, em qualquer expressão que você escreve, cada variável é modificada no máximo uma vez e, se uma variável é modificada, você também não tenta usar o valor dessa variável em outro lugar.
fonte
O motivo é que o programa está executando um comportamento indefinido. O problema está na ordem de avaliação, porque não há pontos de sequência necessários de acordo com o padrão C ++ 98 (nenhuma operação é sequenciada antes ou depois do outro, de acordo com a terminologia do C ++ 11).
No entanto, se você se ater a um compilador, encontrará o comportamento persistente, desde que não adicione chamadas de função ou ponteiros, o que tornaria o comportamento mais confuso.
Então, primeiro o GCC: Usando o Nuwen MinGW 15 GCC 7.1, você obterá:
}
Como o GCC funciona? avalia subexpressões da esquerda para a direita do lado direito (RHS) e atribui o valor ao lado esquerdo (LHS). É exatamente assim que Java e C # se comportam e definem seus padrões. (Sim, o software equivalente em Java e C # definiu comportamentos). Ele avalia cada sub-expressão, uma por uma na Declaração RHS, na ordem da esquerda para a direita; para cada subexpressão: o ++ c (pré-incremento) é avaliado primeiro e depois o valor c é usado para a operação, depois o pós-incremento c ++).
de acordo com o GCC C ++: operadores
o código equivalente no comportamento definido C ++, conforme o GCC entende:
Então vamos ao Visual Studio . Visual Studio 2015, você obtém:
Como o visual studio funciona, adota outra abordagem, avalia todas as expressões de pré-incrementos na primeira passagem, usa valores de variáveis nas operações na segunda passagem, atribui de RHS a LHS na terceira passagem e, na última passagem, avalia todas as expressões pós-incremento em uma passagem.
Portanto, o equivalente no comportamento definido C ++ como o Visual C ++ entende:
como a documentação do Visual Studio declara em Precedência e ordem de avaliação :
fonte