C Encerrar o Programa: exit e atexit

Todo programa termina, e ao terminar ele deixa um recado para quem o chamou: um número que diz "deu certo" ou "deu errado". É esse número que você vem escrevendo no return 0; do main. Nesta aula você vai ver quem lê esse recado, como encerrar o programa de qualquer lugar com exit, o que acontece com os arquivos e com a saída no momento do fim, e como deixar uma função de "fechar a loja" que roda sozinha quando o programa termina, com atexit.

Você vai aprender
  • O que é o código de saída e como vê-lo no terminal com echo $?.
  • Por que return 1; no main e exit(1); são a mesma coisa, e quando só o exit resolve.
  • EXIT_SUCCESS, EXIT_FAILURE e os valores de 0 a 255.
  • O que o exit faz antes de sair, e o que se perde com _Exit e abort.
  • Como registrar funções de limpeza com atexit, e em que ordem elas rodam.
  • O atributo [[noreturn]] C23 para funções que nunca voltam.

Você vai precisar de Funções, Argumentos da Linha de Comando e Arquivos. Todos os programas foram compilados com o gcc 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c, com gcc -std=c23 -Wall -Wextra arquivo.c -o programa.


A ideia: o recado no fim do expediente

Quando a funcionária da lojinha vai embora, ela deixa um bilhete para a Ana: "tudo certo" ou "faltou troco no caixa". A Ana não precisa saber de tudo o que aconteceu no dia; basta o bilhete para decidir o que fazer amanhã. E, antes de sair, a funcionária segue um ritual: apaga as luzes, fecha o caixa, tranca a porta.

Um programa faz o mesmo. O bilhete é o código de saída (um número: 0 = tudo certo, outro número = algo deu errado). O ritual são as tarefas de fim: esvaziar o que ainda não foi escrito na tela, fechar os arquivos e rodar as funções que você pediu com atexit.


1. O código de saída e o echo $?

Este programa confere se um arquivo existe e devolve um código diferente para cada situação:

#include <stdio.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Uso: %s ARQUIVO\n", argv[0]);
        return 2;
    }
    FILE *f = fopen(argv[1], "r");
    if (f == NULL) {
        printf("%s: não encontrei\n", argv[1]);
        return 1;
    }
    fclose(f);
    printf("%s: existe\n", argv[1]);
    return 0;
}
return 2;
Chamado do jeito errado (faltou o nome do arquivo). Muitos comandos do Linux usam 2 para "uso errado".
return 1;
O arquivo não existe: deu errado.
return 0;
Tudo certo. 0 é sucesso, ao contrário do que a intuição diz: há um jeito só de dar certo e muitos de dar errado, e cada um pode ganhar seu número.

O terminal guarda o código do último programa numa variável especial do shell, a $?. O comando echo $? mostra o valor:

$ gcc -std=c23 -Wall -Wextra existe.c -o existe
$ ./existe lista.txt
lista.txt: existe
$ echo $?
0
$ ./existe compras.txt
compras.txt: não encontrei
$ echo $?
1
$ ./existe
Uso: ./existe ARQUIVO
$ echo $?
2

Quem lê o código não é a pessoa, é outro programa: o shell, um script, o make. No Bash, && quer dizer "se o anterior deu certo (0), rode este" e ||, "se deu errado, rode este":

$ ./existe lista.txt && echo "Pode abrir."
lista.txt: existe
Pode abrir.
$ ./existe compras.txt && echo "Pode abrir."
compras.txt: não encontrei                       « deu 1: o echo não rodou
$ ./existe compras.txt || echo "Crie o arquivo primeiro."
compras.txt: não encontrei
Crie o arquivo primeiro.

É assim que o make sabe que o gcc falhou e para tudo (aula Makefile), e é por isso que um programa que deu errado não deve terminar com 0, mesmo que já tenha mostrado a mensagem de erro.

No editor do guia, o código de saída aparece na última linha da caixa: [código de saída: 1]. Não há echo $?, porque não há um shell de verdade.

2. exit: encerrar de dentro de uma função

O return sai da função em que está. No main, sair da função é sair do programa. Mas, numa função qualquer, o return só volta para quem chamou, e o programa continua. Quando um erro é tão grave que não faz sentido continuar, use exit (do stdlib.h), que encerra o programa inteiro, de qualquer lugar:

#include <stdio.h>
#include <stdlib.h>

int ler_volume(const char *arquivo) {
    FILE *f = fopen(arquivo, "r");
    if (f == NULL) {
        perror(arquivo);
        exit(EXIT_FAILURE);
    }
    int volume = 0;
    if (fscanf(f, "volume=%d", &volume) != 1) {
        fprintf(stderr, "%s: formato errado\n", arquivo);
        fclose(f);
        exit(EXIT_FAILURE);
    }
    fclose(f);
    return volume;
}

int main(void) {
    int volume = ler_volume("config.txt");
    printf("Volume: %d\n", volume);
    return EXIT_SUCCESS;
}
#include <stdlib.h>
Onde moram exit, EXIT_SUCCESS e EXIT_FAILURE.
perror(arquivo);
Mostra o motivo da falha do fopen (aula errno e perror).
exit(EXIT_FAILURE);
Encerra o programa aqui mesmo, com o código de "falhou". O main nunca recebe um volume inventado, e o printf("Volume…") não roda.
fclose(f); exit(EXIT_FAILURE);
O exit fecharia o arquivo sozinho (seção 4), mas fechar o que você abriu é um bom hábito.
return EXIT_SUCCESS;
O mesmo que return 0;, com um nome que diz o que significa.
$ gcc -std=c23 -Wall -Wextra config.c -o config
$ ./config
config.txt: No such file or directory
$ echo $?
1
$ echo 'volume=7' > config.txt
$ ./config
Volume: 7
$ echo $?
0
$ echo 'vol 7' > config.txt
$ ./config
config.txt: formato errado
$ echo $?
1

EXIT_SUCCESS e EXIT_FAILURE são constantes do stdlib.h. No Linux, no Windows, no macOS e no editor do guia, valem 0 e 1:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    printf("EXIT_SUCCESS = %d\n", EXIT_SUCCESS);
    printf("EXIT_FAILURE = %d\n", EXIT_FAILURE);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra valores.c -o valores
$ ./valores
EXIT_SUCCESS = 0
EXIT_FAILURE = 1

E no main? Lá, return N; e exit(N); fazem a mesma coisa: o C define que voltar do main é chamar exit com o valor devolvido. E se o main chega ao } sem return, é como return 0; C99 (aula Retorno). Use return no main e exit fora dele.

Missão: esta caixa não tem o config.txt, então o fopen falha. Mas a função faz return 0; e o programa segue mostrando Volume: 0, terminando com código 0 como se estivesse tudo bem. Troque esse return 0; por exit(EXIT_FAILURE); e rode: a última linha deve ser [código de saída: 1], sem o "Volume".

#include <stdio.h>
#include <stdlib.h>

int ler_volume(const char *arquivo) {
    FILE *f = fopen(arquivo, "r");
    if (f == NULL) {
        perror(arquivo);
        return 0;
    }
    int volume = 0;
    if (fscanf(f, "volume=%d", &volume) != 1) {
        volume = 0;
    }
    fclose(f);
    return volume;
}

int main(void) {
    int volume = ler_volume("config.txt");
    printf("Volume: %d\n", volume);
    return EXIT_SUCCESS;
}
#include <stdio.h>
#include <stdlib.h>

int ler_volume(const char *arquivo) {
    FILE *f = fopen(arquivo, "r");
    if (f == NULL) {
        perror(arquivo);
        exit(EXIT_FAILURE);
    }
    int volume = 0;
    if (fscanf(f, "volume=%d", &volume) != 1) {
        volume = 0;
    }
    fclose(f);
    return volume;
}

int main(void) {
    int volume = ler_volume("config.txt");
    printf("Volume: %d\n", volume);
    return EXIT_SUCCESS;
}

3. Os valores: de 0 a 255

O código de saída é um int no C, mas o Linux só guarda um byte dele: de 0 a 255. Valores maiores "dão a volta" (o resto da divisão por 256):

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        return 2;
    }
    int n = atoi(argv[1]);
    exit(n);
}
$ gcc -std=c23 -Wall -Wextra codigos.c -o codigos
$ ./codigos 255; echo $?
255
$ ./codigos 256; echo $?
0                « 256 deu a volta e virou "sucesso"!
$ ./codigos 300; echo $?
44               « 300 - 256
$ ./codigos -1; echo $?
255

O ; no terminal só separa dois comandos na mesma linha. Repare no perigo do 256: um programa que devolvesse "a quantidade de erros" diria "sucesso" com 256 erros. Por isso, use números pequenos. As convenções do Linux:

CódigoQuem usaSignificado
0TodosDeu certo
1A maioria dos programasErro geral
2Muitos comandos (ls, grep, o Bash)Uso errado ou problema sério
126, 127O shellO programa não pôde rodar / não foi encontrado
128 + NO shellO programa morreu pelo sinal N: 134 = 128 + 6 (abort), 139 = 128 + 11 (falha de segmentação)
$ ls /nao-existe; echo $?
ls: não foi possível acessar '/nao-existe': Arquivo ou diretório inexistente
2
$ nao-existe; echo $?
bash: nao-existe: comando não encontrado
127

Experimente: o campo Argumentos começa com 300. Rode e veja o [código de saída: 44]. Depois tente 256, -1, 255 e 1000. O editor faz a mesma conta que o Linux.

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        return 2;
    }
    int n = atoi(argv[1]);
    exit(n);
}

4. O que o exit faz antes de sair

O exit não "desliga a tomada". Ele segue um ritual, nesta ordem:

  1. Roda as funções registradas com atexit (seção 5), da última para a primeira.
  2. Esvazia os buffers: tudo o que o printf e o fprintf guardaram e ainda não escreveram vai para a tela ou para o arquivo.
  3. Fecha todos os arquivos abertos.
  4. Entrega o código de saída ao sistema.

O passo 2 importa porque o printf não escreve na hora: ele junta o texto num buffer (uma área de espera) e escreve de uma vez, no \n ou quando o buffer enche (aula stdin, stdout e stderr). Para ver o ritual funcionando, compare exit com _Exit C99, que sai sem ritual nenhum:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    FILE *f = fopen("vendas.txt", "w");
    if (f == NULL) {
        perror("vendas.txt");
        return 1;
    }
    fprintf(f, "3 pães\n");
    printf("Venda anotada");
    exit(0);
}

O programa não fecha o arquivo e o printf não tem \n: os dois textos ainda estão nos buffers quando o exit é chamado. O fechar2.c é igual, com _Exit(0); na última linha.

$ gcc -std=c23 -Wall -Wextra fechar.c -o fechar
$ ./fechar; echo; cat vendas.txt
Venda anotada
3 pães
$ rm vendas.txt
$ gcc -std=c23 -Wall -Wextra fechar2.c -o fechar2
$ ./fechar2; echo; wc -c vendas.txt
                       « nada na tela
0 vendas.txt           « o arquivo foi criado, mas ficou com 0 bytes
./fechar; echo; cat vendas.txt
Roda o programa, pula uma linha (o echo sozinho só escreve um Enter, porque o printf não tinha \n) e mostra o arquivo.
exit(0)
Esvaziou os dois buffers: o "Venda anotada" apareceu e o "3 pães" foi gravado.
_Exit(0)
Saiu na hora. O que estava nos buffers se perdeu: nem a tela nem o arquivo receberam nada. wc -c conta os bytes do arquivo.

O abort() também sai sem ritual, e ainda avisa o sistema de que o programa morreu de propósito, por um erro grave (é o que o assert usa quando falha; aula Testes). O código é 134:

#include <stdio.h>
#include <stdlib.h>

void tchau(void) {
    printf("Até logo!\n");
}

int main(void) {
    atexit(tchau);
    printf("Algo muito errado aconteceu.\n");
    abort();
}
$ gcc -std=c23 -Wall -Wextra aborta.c -o aborta
$ ./aborta
Algo muito errado aconteceu.
Abortado                   (imagem do núcleo gravada) ./aborta
$ echo $?
134

O "Até logo!" registrado com atexit não apareceu: o abort não roda o ritual. O mesmo vale quando o programa morre sozinho (falha de segmentação, divisão por zero): nenhuma função de atexit roda.

Como terminaRoda o atexit?Esvazia buffers e fecha arquivos?Código
return N; no mainSimSimN
exit(N);SimSimN
_Exit(N); C99NãoNãoN
abort();NãoNão134
Falha de segmentaçãoNãoNão139

5. atexit: o ritual de fim do expediente

atexit = "at exit", "na saída". Você passa o nome de uma função, e o C guarda esse nome para chamá-la quando o programa terminar normalmente (pelo return do main ou por um exit, de onde for):

#include <stdio.h>
#include <stdlib.h>

void apagar_luzes(void) {
    printf("Luzes apagadas.\n");
}

void fechar_caixa(void) {
    printf("Caixa fechado.\n");
}

void trancar_porta(void) {
    printf("Porta trancada.\n");
}

int main(void) {
    atexit(trancar_porta);
    atexit(fechar_caixa);
    atexit(apagar_luzes);
    printf("Lojinha aberta.\n");
    printf("Fim do expediente.\n");
    return 0;
}
void apagar_luzes(void)
Uma função de atexit tem sempre essa forma: não recebe nada e não devolve nada.
atexit(trancar_porta);
Registra a função, sem parênteses depois do nome: você não está chamando trancar_porta agora, está entregando a função para ser chamada depois (um ponteiro para função, aula Ponteiros para Função).
return 0;
Ao terminar, o C chama as funções registradas na ordem inversa: a última registrada roda primeiro.
$ gcc -std=c23 -Wall -Wextra ordem.c -o ordem
$ ./ordem
Lojinha aberta.
Fim do expediente.
Luzes apagadas.
Caixa fechado.
Porta trancada.

Por que de trás para a frente? Pense numa pilha de pratos: o último que você pôs é o primeiro que tira. Na vida real, quem abre a loja destranca a porta, depois abre o caixa, depois acende as luzes; para fechar, desfaz na ordem contrária. O atexit segue essa lógica: o que foi preparado por último é desfeito primeiro.

Quando usar. O atexit é útil para limpezas que precisam acontecer por qualquer caminho de saída: salvar um arquivo de estado, apagar um arquivo temporário, mostrar um resumo. Se o programa tem dez exit espalhados, você não precisa lembrar da limpeza em cada um. O C garante pelo menos 32 registros. Como a função não recebe parâmetros, o que ela usa costuma ficar numa variável global (aula Escopo).

Missão: a Ana registrou as funções na ordem em que pensou nelas, e o fim saiu ao contrário: a porta foi trancada com as luzes ainda acesas. Mude a ordem dos três atexit para que o programa termine com Luzes apagadas., Caixa fechado. e Porta trancada., nessa ordem.

#include <stdio.h>
#include <stdlib.h>

void apagar_luzes(void) {
    printf("Luzes apagadas.\n");
}

void fechar_caixa(void) {
    printf("Caixa fechado.\n");
}

void trancar_porta(void) {
    printf("Porta trancada.\n");
}

int main(void) {
    atexit(apagar_luzes);
    atexit(fechar_caixa);
    atexit(trancar_porta);
    printf("Lojinha aberta.\n");
    printf("Fim do expediente.\n");
    return 0;
}
#include <stdio.h>
#include <stdlib.h>

void apagar_luzes(void) {
    printf("Luzes apagadas.\n");
}

void fechar_caixa(void) {
    printf("Caixa fechado.\n");
}

void trancar_porta(void) {
    printf("Porta trancada.\n");
}

int main(void) {
    atexit(trancar_porta);
    atexit(fechar_caixa);
    atexit(apagar_luzes);
    printf("Lojinha aberta.\n");
    printf("Fim do expediente.\n");
    return 0;
}

6. [[noreturn]]: a função que nunca volta

É comum criar uma função "erro fatal" que mostra a mensagem e chama exit. O compilador, porém, não sabe que ela nunca volta, e reclama de quem a usa:

#include <stdio.h>
#include <stdlib.h>

void erro_fatal(const char *msg) {
    fprintf(stderr, "Erro: %s\n", msg);
    exit(EXIT_FAILURE);
}

int dividir(int a, int b) {
    if (b != 0) {
        return a / b;
    }
    erro_fatal("divisão por zero");
}

int main(void) {
    printf("10 / 2 = %d\n", dividir(10, 2));
    printf("10 / 0 = %d\n", dividir(10, 0));
    return 0;
}
$ gcc -std=c23 -Wall -Wextra fatal.c -o fatal
fatal.c: In function ‘dividir’:
fatal.c:14:1: warning: control reaches end of non-void function [-Wreturn-type]
   14 | }
      | ^

"O controle chega ao fim de uma função que não é void": o gcc acha que, depois do erro_fatal, a dividir termina sem devolver nada. Para contar a verdade ao compilador, marque a função com o atributo [[noreturn]] C23 ("não volta"; aula Atributos):

[[noreturn]] void erro_fatal(const char *msg) {
    fprintf(stderr, "Erro: %s\n", msg);
    exit(EXIT_FAILURE);
}
$ gcc -std=c23 -Wall -Wextra fatal2.c -o fatal2
$ ./fatal2
10 / 2 = 5
Erro: divisão por zero
$ echo $?
1

Sem aviso. O próprio exit é declarado assim no stdlib.h: se a dividir chamasse exit(EXIT_FAILURE); direto, no lugar do erro_fatal, o gcc também não reclamaria. Antes do C23, escrevia-se _Noreturn void erro_fatal(…) C11, ou noreturn com #include <stdnoreturn.h>; essas formas ainda funcionam.


Formas diferentes, e qual usar

FormaQuando usar
return 0; / return 1; no mainO jeito normal de terminar
return EXIT_SUCCESS; / EXIT_FAILUREO mesmo, com nomes que se explicam; comum em código que quer ser portátil
exit(EXIT_FAILURE); numa funçãoErro grave, longe do main, em que não faz sentido continuar
Devolver um código de erro e deixar o main decidirFunções que outros programas vão reaproveitar (uma biblioteca não deve encerrar o programa de quem a usa)
atexit(funcao);Limpeza que tem de acontecer por qualquer caminho de saída
_Exit(N);Raro: sair sem limpar nada (por exemplo, num processo filho depois de um fork)
abort();Um erro "impossível", do programador: melhor morrer na hora e deixar o rastro (o assert faz isso)

Quando dá errado

error: implicit declaration of function ‘exit’
$ gcc -std=c23 -Wall -Wextra semstdlib.c -o semstdlib
semstdlib.c: In function ‘main’:
semstdlib.c:5:5: error: implicit declaration of function ‘exit’ [-Wimplicit-function-declaration]
    5 |     exit(1);
      |     ^~~~
semstdlib.c:2:1: note: include ‘<stdlib.h>’ or provide a declaration of ‘exit’
    1 | #include <stdio.h>
  +++ |+#include <stdlib.h>
    2 |
semstdlib.c:5:5: warning: incompatible implicit declaration of built-in function ‘exit’ [-Wbuiltin-declaration-mismatch]
    5 |     exit(1);
      |     ^~~~
semstdlib.c:5:5: note: include ‘<stdlib.h>’ or provide a declaration of ‘exit’

O que significa: o programa usa exit sem o #include <stdlib.h>. O próprio gcc mostra onde pôr a linha (o +++ |+). No editor, o Clang diz call to undeclared library function 'exit' e include the header <stdlib.h>.

Como consertar: #include <stdlib.h> no topo. O mesmo vale para atexit, _Exit, abort e as constantes EXIT_….

error: passing argument 1 of ‘atexit’ from incompatible pointer type
$ gcc -std=c23 -Wall -Wextra assinatura.c -o assinatura
assinatura.c: In function ‘main’:
assinatura.c:9:12: error: passing argument 1 of ‘atexit’ from incompatible pointer type [-Wincompatible-pointer-types]
    9 |     atexit(tchau);
      |            ^~~~~
      |            |
      |            void (*)(int)
In file included from assinatura.c:2:
/usr/include/stdlib.h:756:27: note: expected ‘void (*)(void)’ but argument is of type ‘void (*)(int)’
  756 | extern int atexit (void (*__func) (void)) __THROW __nonnull ((1));
      |                    ~~~~~~~^~~~~~~~~~~~~~
assinatura.c:4:6: note: ‘tchau’ declared here
    4 | void tchau(int codigo) {
      |      ^~~~~

O que significa: a função tchau recebe um int, e o atexit só aceita funções void f(void). O void (*)(void) da nota é "ponteiro para função que não recebe nada e não devolve nada".

Como consertar: escreva a função como void tchau(void). Se ela precisa de um valor, guarde-o numa variável global antes do exit.

error: invalid use of void expression no atexit
$ gcc -std=c23 -Wall -Wextra parenteses.c -o parenteses
parenteses.c: In function ‘main’:
parenteses.c:9:12: error: invalid use of void expression
    9 |     atexit(tchau());
      |            ^~~~~~~

O que significa: "uso inválido de uma expressão void". com os parênteses, você chamou a tchau agora e tentou passar o resultado dela (que é void, nada) para o atexit.

Como consertar: atexit(tchau);, só o nome.

O texto do printf sumiu

O que significa: o programa terminou com _Exit ou abort (ou morreu por um erro) enquanto o texto ainda estava no buffer. Foi o que aconteceu com o fechar2: nem a tela nem o vendas.txt receberam nada.

Como consertar: termine com return ou exit. Se precisa mesmo sair sem ritual, chame fflush(stdout); e fclose antes.

warning: ‘noreturn’ function does return
$ gcc -std=c23 -Wall -Wextra noreturnerr.c -o noreturnerr
noreturnerr.c: In function ‘erro_fatal’:
noreturnerr.c:6:1: warning: ‘noreturn’ function does return
    6 | }
      | ^
$ ./noreturnerr
Erro: teste
Instrução ilegal         (imagem do núcleo gravada) ./noreturnerr
$ echo $?
132

O que significa: a função foi marcada [[noreturn]], mas esqueceram o exit dentro dela. Voltar de uma função [[noreturn]] é comportamento indefinido: o gcc confiou na promessa, não preparou a volta e pôs uma "instrução ilegal" no lugar. No editor do guia, o mesmo programa morre com Falha de segmentação (139), e o Clang avisa com function declared 'noreturn' should not return.

Como consertar: toda função [[noreturn]] tem de terminar com exit, abort ou um laço infinito.

O programa mostrou o erro, mas o make (ou o script) continuou como se estivesse tudo bem

O que significa: depois da mensagem de erro, o programa terminou com 0 (esqueceram o return 1;, ou o main chegou ao fim). Quem chama só olha o código.

Como consertar: todo caminho de erro termina com um código diferente de 0. Teste com ./programa; echo $?.

O segundo echo $? mostrou 0
$ ./existe compras.txt
compras.txt: não encontrei
$ echo $?
1
$ echo $?
0

O que significa: o $? guarda o código do último comando, e o último foi o primeiro echo, que deu certo.

Como consertar: olhe o $? logo depois do programa, ou guarde-o: ./existe x; codigo=$?.


Referência rápida

EscritaCabeçalhoO que faz
return N; no main—Termina o programa com o código N (o mesmo que exit(N))
exit(N);stdlib.hTermina de qualquer lugar: roda o atexit, esvazia buffers, fecha arquivos
EXIT_SUCCESS, EXIT_FAILUREstdlib.h0 e 1 (os códigos portáteis de sucesso e falha)
atexit(f);stdlib.hRegistra void f(void) para o fim; ordem inversa; devolve 0 se deu certo
_Exit(N); C99stdlib.hTermina na hora, sem ritual
abort();stdlib.hTermina por erro grave, sem ritual; código 134
quick_exit(N);, at_quick_exit(f); C11stdlib.hSaída rápida que roda só as funções de at_quick_exit (raro)
[[noreturn]] void f(…) C23—Promete que a função nunca volta
echo $?(shell)Mostra o código de saída do último comando
prog && outro / prog || outro(shell)Roda outro se prog deu certo / deu errado

Exercícios

1. Sucesso

Qual código de saída quer dizer "deu certo"?

2. Sair de dentro da função

Complete para encerrar o programa com o código de falha, de dentro de uma função qualquer:

(EXIT_FAILURE);

3. Dando a volta

Qual valor o echo $? mostra depois de um programa que fez exit(260);?

4. O programa fez atexit(a); atexit(b); atexit(c); e terminou com return 0;. Em que ordem as funções rodam?

5. Qual destas saídas não roda as funções do atexit?

6. Qual é a forma certa de uma função para o atexit?

Missão final: o caixa da lojinha

Missão: o caixa vende pães de um estoque de 10. Quando um pedido é maior que o estoque, a função vender deve mostrar o erro e encerrar o programa com o código 3 (hoje ela continua e o estoque fica negativo). E o relatorio tem de rodar no fim, por qualquer caminho: registre-o com atexit no começo do main. A saída deve ser:

Vendi 4. Restam 6.
Vendi 5. Restam 1.
Estoque insuficiente para 3 pães.
Sobraram no estoque: 1
#include <stdio.h>
#include <stdlib.h>

int estoque = 10;

void relatorio(void) {
    printf("Sobraram no estoque: %d\n", estoque);
}

void vender(int qtd) {
    if (qtd > estoque) {
        fprintf(stderr, "Estoque insuficiente para %d pães.\n", qtd);
        // encerre o programa com o código 3
    }
    estoque -= qtd;
    printf("Vendi %d. Restam %d.\n", qtd, estoque);
}

int main(void) {
    // registre o relatorio para rodar no fim
    vender(4);
    vender(5);
    vender(3);
    vender(1);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>

int estoque = 10;

void relatorio(void) {
    printf("Sobraram no estoque: %d\n", estoque);
}

void vender(int qtd) {
    if (qtd > estoque) {
        fprintf(stderr, "Estoque insuficiente para %d pães.\n", qtd);
        exit(3);
    }
    estoque -= qtd;
    printf("Vendi %d. Restam %d.\n", qtd, estoque);
}

int main(void) {
    atexit(relatorio);
    vender(4);
    vender(5);
    vender(3);
    vender(1);
    return 0;
}

Repare que o vender(1) nunca roda: o exit(3) encerrou o programa no meio do vender(3), e mesmo assim o relatório apareceu. No laboratório:

$ gcc -std=c23 -Wall -Wextra caixa.c -o caixa
$ ./caixa
Vendi 4. Restam 6.
Vendi 5. Restam 1.
Estoque insuficiente para 3 pães.
Sobraram no estoque: 1
$ echo $?
3

Resumo

  • Todo programa termina com um código de saída: 0 = deu certo, outro valor = deu errado. No terminal, echo $? mostra o último; && e || decidem por ele.
  • No main, return N; é o mesmo que exit(N);. Fora dele, só o exit (do stdlib.h) encerra o programa.
  • EXIT_SUCCESS e EXIT_FAILURE valem 0 e 1. O Linux guarda só de 0 a 255: exit(256) vira 0.
  • O exit roda o atexit, esvazia os buffers e fecha os arquivos. _Exit, abort e as mortes por erro não fazem nada disso.
  • atexit(f) registra void f(void) para o fim; as funções rodam na ordem inversa do registro.
  • [[noreturn]] C23 diz ao compilador que a função nunca volta; ela precisa mesmo nunca voltar.