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.


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.

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 há 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.


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.


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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.