Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Existem alguns pontos no código onde C faz garantias limitadas sobre a ordem das operações. Esses pontos são chamados de pontos de sequência. Aqui estão os casos em que eles ocorrem:
No final de uma expressão completa: ou seja, uma expressão que não faz parte de uma expressão maior. Todos os efeitos colaterais gerados por essa expressão são realizados antes que a execução passe para o código subsequente.
No final do primeiro operando de certos operadores: ,
, &&
, ||
e ?:
. Todos os efeitos colaterais especificados por esse operando são realizados antes de qualquer execução do próximo operando.
As vírgulas que separam argumentos em uma chamada de função não são operadores vírgula e não criam pontos de sequência. A regra para argumentos de função e a regra para operandos são diferentes (veja Ordenação de Operandos).
Logo antes de chamar uma função: todos os efeitos colaterais gerados pelas expressões dos argumentos são realizados antes de chamar a função.
Se a função a ser chamada não for constante — isto é, se for computada por uma expressão — todos os efeitos colaterais nessa expressão são realizados antes de chamar a função.
A ordenação imposta por um ponto de sequência se aplica localmente a um intervalo limitado de código, conforme indicado em cada caso acima. Por exemplo, a ordenação imposta pelo operador vírgula não se aplica ao código fora dos operandos desse operador. Assim, neste código:
o ponto de sequência do operador vírgula ordena x = 5
antes de foo (x)
, mas x * x
pode ser calculado antes ou depois deles.
Os requisitos de ordenação para as operações de pós-incremento e pós-decremento (veja Pós-incremento e Pós-decremento) são flexíveis: esses efeitos colaterais devem ocorrer "um pouco depois," antes do próximo ponto de sequência. Isso ainda permite várias ordens que podem produzir resultados diferentes. Nesta expressão:
não é previsível se x
será incrementado antes ou depois de chamar a função foo
. Se foo
referir-se a x
, ela pode ver o valor antigo ou o valor incrementado.
A solução para estes e outros problemas de ordem de execução é simples: definir bem os pontos de sequência ao usar instruções separadas. Por exemplo:
x++;
z = x - foo ();
Nesta expressão em particular:
x
certamente será incrementado, mas o valor incrementado pode ser substituído pelo valor antigo. Isso acontece porque a incrementação e a atribuição podem ocorrer em qualquer ordem. Se a incrementação de x
ocorrer depois da atribuição para x
, o valor incrementado permanecerá. Mas, se a incrementação ocorrer primeiro, a atribuição colocará o valor não incrementado de volta em x
, deixando a expressão como um todo sem alterar o valor de x
.
Conclusão: evite tais expressões. Certifique-se, ao usar pós-incremento e pós-decremento, de que a expressão específica que você utiliza não seja ambígua quanto à ordenação de execução.
Um operador binário associativo, como +
, quando usado repetidamente, pode combinar qualquer número de operandos. Os valores dos operandos podem ser calculados em qualquer ordem.
Se os valores forem inteiros e o estouro puder ser ignorado, eles podem ser combinados em qualquer ordem. Assim, dadas quatro funções que retornam unsigned int
, chamá-las e somar seus resultados, como no exemplo:
pode somar os resultados em qualquer ordem.
Por outro lado, a aritmética com inteiros com sinal, em que o estouro é significativo, nem sempre é associativa (veja Estouro de Inteiros). Portanto, as somas devem ser realizadas na ordem especificada, obedecendo os parênteses e a associação à esquerda. Isso significa calcular (foo () + bar ())
e (baz () + quux ())
primeiro (em qualquer ordem), depois somar os dois.
O mesmo se aplica à aritmética com valores de ponto flutuante, já que ela também não é realmente associativa. No entanto, a opção do GCC -funsafe-math-optimizations
permite que o compilador altere a ordem de cálculo quando uma operação associativa (associativa na matemática exata) combina vários operandos. Essa opção entra em vigor ao compilar um módulo (veja ). Alterar a ordem de associação pode permitir que o programa otimize a execução das operações de ponto flutuante.
Em todos esses casos, as quatro chamadas de função podem ser realizadas em qualquer ordem. Não há certo ou errado nisso.
Os operandos e argumentos podem ser computados em qualquer ordem, mas há limites para esse embaralhamento no GNU C:
Operandos de um operador aritmético binário podem ser computados em qualquer ordem, mas não podem ser misturados: um deles precisa ser completamente calculado antes do outro. Quaisquer efeitos colaterais no operando que é computado primeiro são executados antes que o outro operando seja computado.
Isso também se aplica aos operadores de atribuição, exceto na atribuição simples, onde o valor anterior do operando à esquerda não é utilizado.
Os argumentos em uma chamada de função podem ser computados em qualquer ordem, mas também não podem ser misturados. Assim, um argumento é totalmente calculado, depois outro, e assim por diante, até que todos sejam concluídos. Quaisquer efeitos colaterais em um argumento são executados antes que a computação de outro argumento comece.
Essas regras não cobrem os efeitos colaterais causados pelos operadores de pós-incremento e pós-decremento — esses podem ser adiados até o próximo ponto de sequência.
Se quisermos ser rigorosos, o fato é que o GCC pode reordenar os cálculos de muitas outras maneiras, desde que isso não altere o resultado da execução do programa. No entanto, porque isso não altera o resultado do programa, é algo desprezível, a menos que você esteja preocupado com os valores de certas variáveis em determinados momentos vistos por outros processos. Nesses casos, você deve usar volatile
para evitar otimizações que poderiam fazer com que se comportassem de maneira estranha. Veja .
A ordem de execução de um programa em C nem sempre é óbvia e não é necessariamente previsível. Este capítulo descreve no que você pode confiar.
A linguagem C não garante que as operações dentro de uma expressão sejam realizadas na ordem em que aparecem no código. Por exemplo, na expressão:
foo
pode ser chamada primeiro ou bar
pode ser chamada primeiro. Se foo
atualiza um dado e bar
usa esse dado, os resultados podem ser imprevisíveis.
A ordem imprevisível de cálculo de subexpressões também faz diferença quando uma delas contém uma atribuição. Já vimos este exemplo de código problemático:
Nesse caso, o segundo argumento, x
, pode ter um valor diferente dependendo se ele for computado antes ou depois da atribuição no terceiro argumento.
Os pontos de sequência limitam a liberdade do compilador para reordenar operações arbitrariamente, mas as otimizações ainda podem reordená-las se o compilador concluir que isso não alterará os resultados. Assim, neste código:
há um ponto de sequência após cada instrução, então o código deve incrementar x
uma vez antes da atribuição a y
e outra vez depois. No entanto, o incremento de x
não afeta y
ou z
, e a definição de y
não pode afetar x
. Por isso, o código pode ser otimizado para:
Normalmente, isso não tem nenhum efeito além de tornar o programa mais rápido. Mas existem situações especiais em que isso pode causar problemas devido a coisas que o compilador não pode saber, como memória compartilhada. Para limitar a otimização nesses casos, use o qualificador de tipo volatile
(veja ).