Essa função C sempre deve retornar false, mas não

317

Eu me deparei com uma pergunta interessante em um fórum há muito tempo e quero saber a resposta.

Considere a seguinte função C:

f1.c

#include <stdbool.h>

bool f1()
{
    int var1 = 1000;
    int var2 = 2000;
    int var3 = var1 + var2;
    return (var3 == 0) ? true : false;
}

Isso sempre deve retornar false desde então var3 == 3000. A mainfunção fica assim:

main.c

#include <stdio.h>
#include <stdbool.h>

int main()
{
    printf( f1() == true ? "true\n" : "false\n");
    if( f1() )
    {
        printf("executed\n");
    }
    return 0;
}

Como f1()sempre deve retornar false, seria de esperar que o programa imprimisse apenas um falso na tela. Mas depois de compilá-lo e executá-lo, executado também é exibido:

$ gcc main.c f1.c -o test
$ ./test
false
executed

Por que é que? Esse código tem algum tipo de comportamento indefinido?

Nota: Eu compilei com gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2.

Dimitri Podborski
fonte
9
Outros mencionaram que você precisa de um protótipo porque suas funções estão em arquivos separados. Mas mesmo se você copiasse f1()para o mesmo arquivo main(), obteria alguma estranheza: embora seja correto em C ++ usar ()para uma lista de parâmetros vazia, em C usado para uma função com uma lista de parâmetros ainda não definida ( basicamente espera uma lista de parâmetros no estilo K&R após o )). Para estar correto C, você deve alterar seu código para bool f1(void).
uliwitness
1
A main()poderia ser simplificada para int main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; }- este iria mostrar a discrepância melhor.
Palec 10/10
@uliwitness Que tal K&R 1ª ed. (1978) quando não havia void?
• Ho1
@uliwitness Não havia truee falsena K&R 1st ed., portanto não havia tais problemas. Era apenas 0 e diferente de zero para verdadeiro e falso. Não é? Não sei se os protótipos estavam disponíveis naquele momento.
• Ho1
1
O K&R 1st Edn precedeu os protótipos (e o padrão C) em mais de uma década (1978 para o livro versus 1989 para o padrão) - de fato, o C ++ (C com classes) ainda estava no futuro quando o K & R1 foi publicado. Além disso, antes da C99, não havia _Booltipo nem <stdbool.h>cabeçalho.
Jonathan Leffler

Respostas:

396

Conforme observado em outras respostas, o problema é que você usa gcc sem opções de compilador definidas. Se você fizer isso, o padrão será o que é chamado "gnu90", que é uma implementação não padrão do antigo padrão C90 retirado de 1990.

No antigo padrão C90, havia uma falha importante na linguagem C: se você não declarasse um protótipo antes de usar uma função, o padrão seria int func ()(onde ( )significa "aceitar qualquer parâmetro"). Isso altera a convenção de chamada da função func, mas não altera a definição real da função. Como o tamanho boole o tamanho intsão diferentes, seu código chama um comportamento indefinido quando a função é chamada.

Esse perigoso comportamento sem sentido foi corrigido no ano de 1999, com o lançamento do padrão C99. Declarações implícitas de funções foram banidas.

Infelizmente, o GCC até a versão 5.xx ainda usa o padrão C antigo por padrão. Provavelmente, não há razão para você querer compilar seu código como algo que não seja o padrão C. Portanto, você deve dizer explicitamente ao GCC que ele deve compilar seu código como código C moderno, em vez de porcaria GNU não padrão de 25 anos ou mais .

Corrija o problema sempre compilando seu programa como:

gcc -std=c11 -pedantic-errors -Wall -Wextra
  • -std=c11 diz para fazer uma tentativa tímida de compilar de acordo com o padrão C (atual) (informalmente conhecido como C11).
  • -pedantic-errors diz para ele fazer de todo o coração o descrito acima e fornecer erros do compilador quando você escreve um código incorreto que viola o padrão C.
  • -Wall significa dar-me alguns avisos extras que podem ser bons.
  • -Wextra significa dar-me outros avisos extras que podem ser bons.
Lundin
fonte
19
Esta resposta está correta em geral, mas para programas mais complicados -std=gnu11é muito mais provável que funcione conforme o esperado -std=c11, devido a qualquer um ou a todos: necessitar de funcionalidade de biblioteca além do C11 (POSIX, X / Open, etc) disponível no "gnu" modos estendidos, mas suprimidos no modo estrito de conformidade; bugs nos cabeçalhos do sistema que estão ocultos nos modos estendidos, por exemplo, assumindo a disponibilidade de typedefs fora do padrão; uso não intencional de trigrafs (esse recurso incorreto padrão é desativado no modo "gnu").
Zwol 07/04
5
Por razões semelhantes, embora eu geralmente incentive o uso de altos níveis de avisos, não posso suportar o uso dos modos de avisos são erros. -pedantic-errorsé menos problemático do que -Werrormas pode e pode causar a falha na compilação de programas em sistemas operacionais que não estão incluídos no teste do autor original, mesmo quando não há nenhum problema real.
Zwol 07/04
7
@Lundin Pelo contrário, o segundo problema que mencionei (bugs nos cabeçalhos do sistema expostos por modos de conformidade estritos) é onipresente ; Fiz testes extensivos e sistemáticos e não sistemas operacionais amplamente usados ​​que não tenham pelo menos um desses erros (a partir de dois anos atrás, pelo menos). Programas em C que exigem apenas a funcionalidade do C11, sem acréscimos adicionais, também são a exceção e não a regra na minha experiência.
Zwol 07/04
6
@joop Se você usa C bool/ padrão _Bool, pode escrever seu código C de maneira "C ++-esque", onde você assume que todas as comparações e operadores lógicos retornam boolcomo C ++, mesmo que retornem intem C, por razões históricas . Isso tem a grande vantagem de poder usar ferramentas de análise estática para verificar a segurança de tipo de todas essas expressões e expor todos os tipos de erros em tempo de compilação. É também uma maneira de expressar intenção na forma de código de auto-documentação. E menos importante, ele também economiza alguns bytes de RAM.
Lundin
7
Observe que a maioria das novidades do C99 veio dessa porcaria de GNU com mais de 25 anos de idade.
21416 Shahbaz
141

Você não tem um protótipo declarado f1()em main.c, portanto ele é implicitamente definido como int f1(), o que significa que é uma função que pega um número desconhecido de argumentos e retorna um int.

Se inte tiver booltamanhos diferentes, isso resultará em um comportamento indefinido . Por exemplo, na minha máquina, inttem 4 bytes e boolum byte. Como a função está definida para retornar bool, ela coloca um byte na pilha quando retorna. No entanto, como é declarado implicitamente para retornar intdo main.c, a função de chamada tentará ler 4 bytes da pilha.

As opções padrão dos compiladores no gcc não indicam que ele está fazendo isso. Mas se você compilar -Wall -Wextra, obterá o seguinte:

main.c: In function ‘main’:
main.c:6: warning: implicit declaration of function ‘f1’

Para corrigir isso, adicione uma declaração para f1em main.c, antes main:

bool f1(void);

Observe que a lista de argumentos está explicitamente definida como void, o que informa ao compilador que a função não aceita argumentos, em oposição a uma lista de parâmetros vazia, o que significa um número desconhecido de argumentos. A definição f1em f1.c também deve ser alterada para refletir isso.

dbush
fonte
2
Algo que eu costumava fazer em meus projetos (quando eu ainda usava o GCC) era adicionado -Werror-implicit-function-declarationàs opções do GCC, dessa forma, esse já não passa despercebido. Uma opção ainda melhor é -Werrortransformar todos os avisos em erros. Força você a corrigir todos os avisos quando eles aparecerem.
uliwitness
2
Você também não deve usar parênteses vazios, pois isso é um recurso obsoleto. Isso significa que eles podem banir esse código na próxima versão do padrão C.
Lundin
1
@uliwitness Ah. Boa informação para quem vem de C ++ que só se envolver em C.
SeldomNeedy
O valor de retorno geralmente não é colocado na pilha, mas em um registrador. Veja a resposta de Owen. Além disso, você geralmente nunca coloca um byte na pilha, mas um múltiplo do tamanho da palavra.
rsanchez
As versões mais recentes do GCC (5.xx) emitem esse aviso sem os sinalizadores extras.
Overv
36

Acho interessante ver onde realmente ocorre a diferença de tamanho mencionada na excelente resposta de Lundin.

Se você compilar --save-temps, obterá arquivos de montagem que podem ser vistos. Aqui está a parte onde f1()faz a == 0comparação e retorna seu valor:

cmpl    $0, -4(%rbp)
sete    %al

A parte retornada é sete %al. Nas convenções de chamada x86 de C, retornam valores de 4 bytes ou menos (que incluem inte bool) são retornados via registrador %eax. %alé o byte mais baixo de %eax. Portanto, os 3 bytes superiores de %eaxsão deixados em um estado não controlado.

Agora em main():

call    f1
testl   %eax, %eax
je  .L2

Este verifica se o conjunto de %eaxé zero, porque acha que está testando um int.

A adição de uma declaração de função explícita muda main()para:

call    f1
testb   %al, %al
je  .L2

que é o que queremos.

Owen
fonte
27

Por favor compile com um comando como este:

gcc -Wall -Wextra -Werror -std=gnu99 -o main.exe main.c

Resultado:

main.c: In function 'main':
main.c:14:5: error: implicit declaration of function 'f1' [-Werror=impl
icit-function-declaration]
     printf( f1() == true ? "true\n" : "false\n");
     ^
cc1.exe: all warnings being treated as errors

Com essa mensagem, você deve saber o que fazer para corrigi-la.

Edit: Depois de ler um comentário (agora excluído), tentei compilar seu código sem os sinalizadores. Bem, isso me levou a erros do vinculador sem avisos do compilador, em vez de erros do compilador. E esses erros de vinculação são mais difíceis de entender; portanto, mesmo que -std-gnu99não seja necessário, tente sempre usar pelo menos -Wall -Werrorisso poupará muita dor na bunda.

jdarthenay
fonte