Isso é um comportamento C indefinido?

A nossa turma foi questionada sobre esta questão pelo prof de programação em C:

Você recebe o código:

int x=1; printf("%d",++x,x+1); 

Qual saída será sempre produzida?

A maioria dos estudantes disse comportamento indefinido. Alguém pode me ajudar a entender por que isso acontece?

Obrigado pela edição e pelas respostas, mas ainda estou confuso.

A saída é provável que seja 2 em cada caso razoável. Na realidade, o que você tem é um comportamento indefinido.

Especificamente, o padrão diz:

Entre o ponto de seqüência anterior e o seguinte, um object deve ter seu valor armazenado modificado no máximo uma vez pela avaliação de uma expressão. Além disso, o valor anterior deve ser lido apenas para determinar o valor a ser armazenado.

Há um ponto de sequência antes de avaliar os argumentos para uma function e um ponto de sequência após todos os argumentos terem sido avaliados (mas a function ainda não foi chamada). Entre esses dois (isto é, enquanto os argumentos estão sendo avaliados) não há um ponto de seqüência (a menos que um argumento seja uma expressão que inclua um internamente, como o uso do operador && || ou , ).

Isso significa que a chamada para printf está lendo o valor anterior para determinar o valor que está sendo armazenado (isto é, o ++x ) e para determinar o valor do segundo argumento (isto é, o x+1 ). Isso claramente viola o requisito citado acima, resultando em um comportamento indefinido.

O fato de você ter fornecido um argumento extra para o qual nenhum especificador de conversão é fornecido não resulta em um comportamento indefinido. Se você fornecer menos argumentos que os especificadores de conversão, ou se o tipo (promovido) do argumento não corresponder ao do especificador de conversão, você terá um comportamento indefinido – mas a passagem de um parâmetro extra não o fará.

Toda vez que o comportamento de um programa é indefinido, qualquer coisa pode acontecer – a frase clássica é que “demônios podem voar para fora do seu nariz” – embora a maioria das implementações não vá tão longe.

Os argumentos de uma function são conceitualmente avaliados em paralelo (o termo técnico é que não há ponto de seqüência entre sua avaliação). Isso significa que as expressões ++x x+1 podem ser avaliadas nesta ordem, na ordem oposta, ou de alguma forma intercalada . Quando você modifica uma variável e tenta acessar seu valor em paralelo, o comportamento é indefinido.

Com muitas implementações, os argumentos são avaliados em seqüência (embora nem sempre da esquerda para a direita). Então é improvável que você veja nada além de 2 no mundo real.

No entanto, um compilador pode gerar código como este:

  1. Carregue x no registrador r1 .
  2. Calcule x+1 adicionando 1 a r1 .
  3. Calcule ++x adicionando 1 a r1 . Tudo bem porque x foi carregado em r1 . Dado como o compilador foi projetado, a etapa 2 não pode ter r1 modificado, porque isso só poderia acontecer se x fosse lido e também escrito entre dois pontos de sequência. Que é proibido pelo padrão C.
  4. Armazene r1 em x .

E nesse compilador (hipotético, mas correto), o programa imprimiria 3.

( EDIT: passando um argumento extra para printf está correto (§7.19.6.1-2 em N1256 ; graças a Prasoon Saurav ) para apontar isso. Também: adicionado um exemplo.)

A resposta correta é: o código produz um comportamento indefinido.

A razão pela qual o comportamento é indefinido é que as duas expressões ++x x + 1 estão modificando x e lendo x para um motivo não relacionado (para modificação) e essas duas ações não são separadas por um ponto de sequência. Isso resulta em comportamento indefinido em C (e C ++). O requisito é dado em 6.5 / 2 do padrão de linguagem C.

Note que o comportamento indefinido neste caso não tem absolutamente nada a ver com o fato de que a function printf recebe apenas um especificador de formato e dois argumentos reais. Para dar mais argumentos para printf que há especificadores de formato na string de formato é perfeitamente legal em C. Novamente, o problema está enraizado na violação dos requisitos de avaliação de expressão da linguagem C.

Observe também que alguns participantes dessa discussão não compreendem o conceito de comportamento indefinido e insistem em misturá-lo com o conceito de comportamento não especificado . Para melhor ilustrar a diferença, vamos considerar o seguinte exemplo simples

 int inc_x(int *x) { return ++*x; } int x_plus_1(int x) { return x + 1; } int x = 1; printf("%d", inc_x(&x), x_plus_1(x)); 

O código acima é “equivalente” ao original, exceto que as operações que envolvem nosso x são agrupadas em funções. O que vai acontecer neste último exemplo?

Não há comportamento indefinido nesse código. Mas como a ordem de avaliação dos argumentos printf não é especificada , esse código produz um comportamento não especificado , ou seja, é possível que printf seja chamado como printf("%d", 2, 2) ou como printf("%d", 2, 3) . Em ambos os casos, a saída será de fato 2 . No entanto, a diferença importante dessa variante é que todos os accesss a x são agrupados em pontos de seqüência presentes no início e no final de cada function, portanto, essa variante não produz um comportamento indefinido.

Este é exatamente o raciocínio que alguns outros cartazes estão tentando forçar no exemplo original. Mas isso não pode ser feito. O exemplo original produz um comportamento indefinido , que é um animal completamente diferente. Eles aparentemente estão tentando insistir que, na prática, o comportamento indefinido é sempre equivalente a um comportamento não especificado. Esta é uma afirmação totalmente falsa que apenas indica a falta de conhecimento em quem a faz. O código original produz um comportamento indefinido, ponto final.

Para continuar com o exemplo, vamos modificar o exemplo de código anterior para

 printf("%d %d", inc_x(&x), x_plus_1(x)); 

a saída do código se tornará geralmente imprevisível. Pode imprimir 2 2 ou pode imprimir 2 3 . No entanto, observe que, embora o comportamento seja imprevisível, ele ainda não produz o comportamento indefinido . O comportamento não é especificado , não é indefinido . Comportamento não especificado é restrito a duas possibilidades: ou 2 2 ou 2 3 . Comportamento indefinido não está restrito a nada. Pode formatar o seu disco rígido em vez de imprimir algo. Sinta a diferença.

A maioria dos estudantes disse comportamento indefinido. Alguém pode me ajudar a entender por que isso acontece?

Porque a ordem em que os parâmetros da function são calculados não é especificada.

Qual saída será sempre produzida?

Ele produzirá 2 em todos os ambientes em que posso pensar. A interpretação estrita do padrão C99, no entanto, torna o comportamento indefinido porque os accesss a x não atendem aos requisitos que existem entre os pontos de sequência.

A maioria dos estudantes disse comportamento indefinido. Alguém pode me ajudar a entender por que isso acontece?

Agora vou abordar a segunda pergunta que entendo como “Por que a maioria dos alunos de minha class diz que o código mostrado constitui um comportamento indefinido?” e acho que nenhum outro pôster respondeu até agora. Uma parte dos alunos terá lembrado exemplos de valor indefinido de expressões como

 f(++i,i) 

O código que você dá se encheckbox nesse padrão, mas os alunos pensam erroneamente que o comportamento é definido de qualquer maneira, porque printf ignora o último parâmetro. Essa nuance confunde muitos alunos. Outra parte do estudante será tão versada no padrão quanto David Thornley e dirá “comportamento indefinido” pelas razões corretas explicadas acima.

Os pontos feitos sobre o comportamento indefinido estão corretos, mas há uma ruga adicional: o printf pode falhar. Está fazendo o arquivo IO; Existem vários motivos pelos quais ele pode falhar e é impossível eliminá-los sem conhecer o programa completo e o contexto no qual ele será executado.

Ecoando codaddict a resposta é 2.

O printf será chamado com o argumento 2 e será impresso.

Se este código é colocado em um contexto como:

 void do_something() { int x=1; printf("%d",++x,x+1); } 

Então o comportamento dessa function é completamente e sem ambiguidade definido. Não estou, é claro, argumentando que isso é bom ou correto, ou que o valor de x é determinável depois.

A saída será sempre (para 99,98% dos compiladores e sistemas compatíveis com stadard mais importantes) 2.

De acordo com o padrão, isso parece ser, por definição , “comportamento indefinido”, uma definição / resposta que se auto-justifica e que não diz nada sobre o que realmente pode acontecer, e especialmente por quê .

A splint do utilitário (que não é uma ferramenta de verificação de conformidade do std) e, portanto, os programadores de splint, consideram isso como “comportamento não especificado”. Isso significa, basicamente, que a avaliação de (x+1) pode dar 1 + 1 ou 2 + 1, dependendo de quando a atualização de x é realmente feita. Entretanto, como a expressão é descartada (o formato printf lê 1 argumento), a saída não é afetada e ainda podemos dizer que é 2.

undefined.c: 7: 20: O argumento 2 modifica x, usado pelo argumento 3 (a ordem de avaliação dos parâmetros reais é indefinida): printf (“% d \ n”, ++ x, x + 1) O código possui um comportamento não especificado. A ordem de avaliação de parâmetros de function ou subexpressões não é definida, portanto, se um valor for usado e modificado em locais diferentes não separados por uma ordem de avaliação de restrição de ponto de sequência, o resultado da expressão não será especificado.

Como dito anteriormente, o comportamento não especificado afeta apenas a avaliação de (x+1) , não toda a declaração ou outras expressões dela. Assim, no caso de “comportamento não especificado”, podemos dizer que a saída é 2 e ninguém pode objetar.

Mas este não é um comportamento não especificado, parece ser “comportamento indefinido”. E o “comportamento indefinido” parece ter que ser algo que afeta toda a afirmação em vez da expressão única. Isto é devido ao mistério em torno de onde o “comportamento indefinido” realmente ocorre (ou seja, o que exatamente afeta).

Se houvesse motivações para append o “comportamento indefinido” apenas à expressão (x+1) , como no caso do “comportamento não especificado”, ainda poderíamos dizer que a saída é sempre (100%) 2. Anexando o ” comportamento indefinido “apenas para (x+1) significa que não somos capazes de dizer se é 1 + 1 ou 2 + 1; é apenas ” qualquer coisa “. Mas, novamente, que “qualquer coisa” é descartada por causa do printf, e isso significa que a resposta seria “sempre (100%) 2”.

Em vez disso, por causa de assimetrias misteriosas, o “comportamento indefinido” não pode ser anexado apenas ao x+1 , mas na verdade ele deve afetar pelo menos o ++x (que por sinal é o responsável pelo comportamento indefinido), se não a declaração inteira. Se ele infecta apenas a expressão ++x , a saída é um “valor indefinido”, ou seja, qualquer inteiro, por exemplo, -5847834 ou 9032. Se ele infecta a declaração inteira, você pode ver a saída do console, provavelmente você poderia ter para parar o programa com ctrl-c, possivelmente antes que ele comece a sufocar o seu cpu.

De acordo com uma lenda urbana, o “comportamento indefinido” infecta não apenas todo o programa, mas também seu computador e as leis da física, de modo que criaturas misteriosas podem ser criadas pelo seu programa e voar para longe ou comer você.

Nenhuma resposta explica nada com competência sobre o assunto. Eles são apenas um “oh veja o padrão diz isso” (e é apenas uma interpretação, como de costume!). Então, pelo menos, você aprendeu que “padrões existem”, e eles fazem as questões educacionais (já que é claro, não esqueça que seu código está errado , independentemente do behaviorismo indefinido / não especificado e de outros fatos padrões), sem objective as investigações profundas e compreensão.