C stdin, stdout e stderr

Desde o primeiro printf, seus programas escrevem na tela e leem o teclado sem nunca abrir nada. É que todo programa já nasce com três canais abertos: um de entrada e dois de saída. Nesta aula você vai conhecê-los pelo nome, aprender a mandar as mensagens de erro pelo canal certo, ver como o terminal liga esses canais a arquivos e a outros programas com >, < e |, e entender por que, às vezes, as linhas saem numa ordem que o programa não escreveu.

Você vai aprender
  • O que são stdin, stdout e stderr, e os números 0, 1 e 2.
  • Por que printf(…) é o mesmo que fprintf(stdout, …), e quando usar fprintf(stderr, …).
  • Como o terminal redireciona os canais: >, >>, 2>, 2>&1, <, | e /dev/null.
  • Como escrever um filtro: um programa que lê o stdin e escreve no stdout.
  • O que é o buffer da saída, por que ele troca a ordem das linhas e como o fflush resolve.

Você vai precisar de Ler uma Linha 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, e rodados num terminal de verdade.


A ideia: a cozinha com três passagens

Imagine a cozinha da lojinha da Ana. Os pedidos chegam por uma janelinha de entrada. Os pratos prontos saem pelo balcão. E, se falta um ingrediente, o cozinheiro avisa pelo interfone, que vai direto para a Ana, e não para o balcão: ninguém quer um bilhete "acabou o queijo" no meio do prato do cliente.

Um programa tem as mesmas três passagens: o stdin é a janelinha (o que entra), o stdout é o balcão (o resultado) e o stderr é o interfone (avisos e erros). Normalmente as três estão ligadas ao terminal: entra o teclado, os dois tipos de saída vão para a tela. Mas cada uma pode ser ligada a outro lugar, separadamente.


1. Os três canais

#include <stdio.h>

int main(void) {
    printf("1. printf\n");
    fprintf(stdout, "2. fprintf(stdout, ...)\n");
    fprintf(stderr, "3. fprintf(stderr, ...)\n");
    fputs("4. fputs(..., stderr)\n", stderr);
    return 0;
}
printf("…")
Escreve no stdout. É só um atalho para a linha de baixo.
fprintf(stdout, "…")
O fprintf da aula Escrever Arquivos, que recebe onde escrever. stdout é uma variável do tipo FILE *, como a que o fopen devolve, mas já aberta pelo sistema.
fprintf(stderr, "…")
O mesmo, no canal de erros.
fputs("…", stderr)
Escreve um texto sem formatação. Repare na ordem: no fputs, o canal vem depois do texto.
$ gcc -std=c23 -Wall -Wextra canais.c -o canais
$ ./canais
1. printf
2. fprintf(stdout, ...)
3. fprintf(stderr, ...)
4. fputs(..., stderr)

Na tela não há diferença nenhuma: os dois canais de saída estão ligados ao mesmo terminal. A diferença aparece quando você os separa (seção 2).

NomeEm inglêsNúmeroLigado, por padrão, aPara quê
stdinstandard input, entrada padrão0tecladoO que o programa lê: scanf, fgets(…, stdin), getchar
stdoutstandard output, saída padrão1telaO resultado: printf, puts, putchar
stderrstandard error, saída de erros2telaErros, avisos, mensagens de uso: fprintf(stderr, …), perror

Os números são o jeito como o sistema operacional chama cada canal (os "descritores de arquivo"), e é por eles que o terminal os redireciona: o 2 do 2> é o stderr. O perror da aula errno escreve no stderr também.

Experimente: rode e repare na cor: no editor do guia, o que vai para o stderr aparece num tom avermelhado. Na mensagem 2, troque o stdout por stderr e rode de novo. (O editor não tem > nem |: a separação dos canais você vê nos terminais desta aula.)

#include <stdio.h>

int main(void) {
    printf("1. printf\n");
    fprintf(stdout, "2. fprintf(stdout, ...)\n");
    fprintf(stderr, "3. fprintf(stderr, ...)\n");
    fputs("4. fputs(..., stderr)\n", stderr);
    return 0;
}

2. Redirecionar a saída: >, 2> e companhia

Quem liga os canais é o shell (o Bash do terminal), antes de o programa começar. O programa não sabe e não precisa saber para onde está escrevendo. Com o mesmo canais:

$ ./canais > saida.txt
3. fprintf(stderr, ...)
4. fputs(..., stderr)               « só o stderr ficou na tela
$ cat saida.txt
1. printf
2. fprintf(stdout, ...)             « o stdout foi para o arquivo
$ ./canais 2> erros.txt
1. printf
2. fprintf(stdout, ...)
$ cat erros.txt
3. fprintf(stderr, ...)
4. fputs(..., stderr)
$ ./canais 2> /dev/null
1. printf
2. fprintf(stdout, ...)             « os erros foram para o "lixo"

E para juntar os dois num arquivo só, ou acrescentar no fim em vez de apagar:

$ ./canais > tudo.txt 2>&1
$ cat tudo.txt
3. fprintf(stderr, ...)
4. fputs(..., stderr)
1. printf
2. fprintf(stdout, ...)             « ordem trocada! (seção 5)
$ ./canais >> saida.txt
3. fprintf(stderr, ...)
4. fputs(..., stderr)
$ cat saida.txt
1. printf
2. fprintf(stdout, ...)
1. printf
2. fprintf(stdout, ...)             « o >> acrescentou no fim
No terminalO que faz
prog > arqstdout vai para arq (apaga o que havia)
prog >> arqstdout vai para o fim de arq
prog 2> arqstderr vai para arq
prog > arq 2>&1Os dois vão para arq ("o 2 vai para onde o 1 está agora")
prog &> arqO mesmo, forma curta do Bash
prog 2> /dev/nullJoga o stderr fora (/dev/null é um "arquivo" que engole tudo)
prog < arqstdin vem de arq (seção 3)
prog1 | prog2O stdout de prog1 vira o stdin de prog2 (o "cano", pipe)

3. Por que separar: o resultado limpo

A Ana tem as notas de uma aluna num arquivo, uma por linha, e uma delas foi digitada errado:

$ cat notas.txt
8.5
7
sete
9

Este programa calcula a média do que vier pelo stdin. O resultado vai para o stdout; os avisos, para o stderr:

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

int main(void) {
    char linha[100];
    int lidas = 0;
    int numero_linha = 0;
    double soma = 0.0;
    while (fgets(linha, sizeof linha, stdin) != NULL) {
        numero_linha++;
        linha[strcspn(linha, "\n")] = '\0';
        char *fim;
        double nota = strtod(linha, &fim);
        if (fim == linha || *fim != '\0') {
            fprintf(stderr, "linha %d ignorada: \"%s\"\n", numero_linha, linha);
            continue;
        }
        soma += nota;
        lidas++;
    }
    if (lidas == 0) {
        fprintf(stderr, "nenhuma nota lida\n");
        return 1;
    }
    printf("%d notas, média %.2f\n", lidas, soma / lidas);
    return 0;
}
while (fgets(linha, sizeof linha, stdin) != NULL)
Lê o stdin até o fim, como na aula Ler uma Linha. O programa não sabe se as linhas vêm do teclado ou de um arquivo.
strtod(linha, &fim)
Converte a nota e confere se a linha era só um número (aula Validar a Entrada).
fprintf(stderr, "linha %d ignorada…")
Um aviso para a pessoa, e não parte do resultado. O continue pula para a próxima linha.
printf("%d notas, média %.2f\n", …)
O resultado, no stdout.
$ gcc -std=c23 -Wall -Wextra media.c -o media
$ ./media < notas.txt
linha 3 ignorada: "sete"
3 notas, média 8.17
$ ./media < notas.txt > resultado.txt
linha 3 ignorada: "sete"            « o aviso continua na tela
$ cat resultado.txt
3 notas, média 8.17                « o arquivo só tem o resultado
$ ./media < notas.txt 2> /dev/null
3 notas, média 8.17

Esse é o motivo de existir o stderr: quem salva a saída num arquivo (ou a passa para outro programa) recebe só o resultado, e a pessoa na frente do terminal continua vendo os avisos. Se o aviso tivesse ido pelo printf, ele estaria dentro do resultado.txt, e o próximo programa que lesse esse arquivo tropeçaria nele (veja em Quando dá errado).

Missão: nesta versão, os dois avisos saem pelo printf e iriam parar no arquivo de resultado. Troque esses dois printf por fprintf(stderr, …), deixando o printf do resultado como está. As notas da Ana já estão na Entrada. Depois de rodar, repare na cor do aviso.

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

int main(void) {
    char linha[100];
    int lidas = 0;
    int numero_linha = 0;
    double soma = 0.0;
    while (fgets(linha, sizeof linha, stdin) != NULL) {
        numero_linha++;
        linha[strcspn(linha, "\n")] = '\0';
        char *fim;
        double nota = strtod(linha, &fim);
        if (fim == linha || *fim != '\0') {
            printf("linha %d ignorada: \"%s\"\n", numero_linha, linha);
            continue;
        }
        soma += nota;
        lidas++;
    }
    if (lidas == 0) {
        printf("nenhuma nota lida\n");
        return 1;
    }
    printf("%d notas, média %.2f\n", lidas, soma / lidas);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
    char linha[100];
    int lidas = 0;
    int numero_linha = 0;
    double soma = 0.0;
    while (fgets(linha, sizeof linha, stdin) != NULL) {
        numero_linha++;
        linha[strcspn(linha, "\n")] = '\0';
        char *fim;
        double nota = strtod(linha, &fim);
        if (fim == linha || *fim != '\0') {
            fprintf(stderr, "linha %d ignorada: \"%s\"\n", numero_linha, linha);
            continue;
        }
        soma += nota;
        lidas++;
    }
    if (lidas == 0) {
        fprintf(stderr, "nenhuma nota lida\n");
        return 1;
    }
    printf("%d notas, média %.2f\n", lidas, soma / lidas);
    return 0;
}

4. Redirecionar a entrada: < e |

Na seção 3, o < notas.txt ligou o stdin a um arquivo. Sem ele, o mesmo programa lê o teclado, até a Ana apertar Ctrl+D:

$ ./media
8.5
nove
linha 2 ignorada: "nove"
10
2 notas, média 9.25                « depois do Ctrl+D

E com o |, a entrada vem da saída de outro programa. Aqui, o cat escreve o arquivo no stdout dele, e o cano o entrega ao stdin do media:

$ cat notas.txt | ./media
linha 3 ignorada: "sete"
3 notas, média 8.17
$ ./media < /dev/null
nenhuma nota lida                  « /dev/null: uma entrada que já começa no fim
$ echo $?
1
$ ./media < faltando.txt
bash: faltando.txt: Arquivo ou diretório inexistente

A última mensagem começa com bash:: foi o shell que não achou o arquivo, e o media nem chegou a rodar. Com o <, quem abre o arquivo é o shell; o programa só recebe o stdin pronto. (Para abrir um arquivo pelo nome, dentro do programa, use fopen, aula Arquivos.)

No editor do guia, o campo Entrada da caixa (o data-stdin das páginas) faz o papel do <: o programa recebe aquelas linhas no stdin e depois o fim da entrada.

5. Filtros: ler o stdin, escrever o stdout

Um filtro é um programa que lê o stdin, transforma e escreve no stdout. Muitos comandos do Linux são filtros (sort, grep, wc), e é isso que permite encaixá-los com |. Este transforma o texto em maiúsculas, um caractere por vez:

#include <ctype.h>
#include <stdio.h>

int main(void) {
    int c;
    while ((c = getchar()) != EOF) {
        putchar(toupper(c));
    }
    return 0;
}
int c;
Um int, e não char, para caber o EOF (aula char).
getchar()
Lê um caractere do stdin. É o mesmo que getc(stdin).
toupper(c)
"to upper", para maiúscula (do ctype.h). O que não é letra minúscula passa igual, inclusive o \n.
putchar(…)
Escreve um caractere no stdout. É o mesmo que putc(…, stdout).
$ gcc -std=c23 -Wall -Wextra maiusculas.c -o maiusculas
$ echo "arroz e leite" | ./maiusculas
ARROZ E LEITE
$ ./maiusculas < lista.txt > LISTA.txt
$ cat LISTA.txt
ARROZ
LEITE
OVOS

Na segunda linha, o programa leu de um arquivo e escreveu em outro, sem ter um único fopen. Um filtro bem-comportado escreve só o texto transformado no stdout; qualquer conversa com a pessoa (avisos, resumos) vai para o stderr, para não estragar o arquivo de saída. É o que a missão final pede.


6. O buffer: por que a ordem muda

Lembra do tudo.txt da seção 2, com as linhas 3 e 4 antes da 1 e da 2? O motivo é o buffer. Escrever no terminal ou no disco custa caro, então o stdout junta o texto numa área de espera e só escreve de tempos em tempos. O stderr, não: erro tem pressa, e cada mensagem sai na hora.

CanalQuando escreve de verdade
stdout ligado ao terminalA cada \n (buffer de linha), quando o programa vai ler o teclado, ou no fim
stdout ligado a um arquivo ou canoQuando junta 4.096 bytes (buffer cheio), ou no fim do programa
stderrNa hora (sem buffer)

No tudo.txt, o stdout ia para um arquivo: as linhas 1 e 2 ficaram esperando no buffer até o fim do programa, enquanto as 3 e 4 foram escritas na hora. O mesmo acontece com o |:

$ ./canais | cat
3. fprintf(stderr, ...)
4. fputs(..., stderr)
1. printf
2. fprintf(stdout, ...)

Mesmo no terminal dá para ver o efeito, quando falta o \n:

#include <stdio.h>

int main(void) {
    printf("Salvando...");
    fprintf(stderr, "aviso: disco quase cheio\n");
    printf(" pronto.\n");
    return 0;
}
$ gcc -std=c23 -Wall -Wextra salvando.c -o salvando
$ ./salvando
aviso: disco quase cheio
Salvando... pronto.               « o "Salvando..." esperou o \n

Para mandar o buffer para a tela (ou para o arquivo) agora, use fflush(stdout); ("flush", dar descarga). Esta versão escreve um log com \n em todas as linhas, mas o log vai para um arquivo, onde o \n não basta. Compare sem e com o fflush:

#include <stdio.h>

int main(void) {
    printf("Salvando...\n");
    fflush(stdout);
    fprintf(stderr, "aviso: disco quase cheio\n");
    printf("Pronto.\n");
    return 0;
}
$ ./salvando2 > log.txt 2>&1          « igual, sem o fflush
$ cat log.txt
aviso: disco quase cheio
Salvando...
Pronto.
$ gcc -std=c23 -Wall -Wextra salvando3.c -o salvando3
$ ./salvando3 > log.txt 2>&1
$ cat log.txt
Salvando...
aviso: disco quase cheio
Pronto.
fflush(stdout);
Escreve já tudo o que está no buffer do stdout. Use antes de uma mensagem no stderr quando a ordem importa, ou para mostrar um progresso (printf(".") sem \n) que tem de aparecer na hora.

O fim do programa também esvazia o buffer (é o ritual do exit, aula exit e atexit). Mas, se o programa morre, o que estava no buffer se perde, e isso confunde muita gente que salva a saída num arquivo para procurar um erro (veja em Quando dá errado).

No editor do guia, o stdout se comporta como num terminal (buffer de linha), então o salvando.c mostra o aviso primeiro, como no terminal da Ana. O caso do arquivo e do cano (buffer cheio) só aparece no terminal de verdade.

Qual canal usar

O que o programa escreveCanal
O resultado: a média, o texto transformado, o relatóriostdout (printf, puts, putchar)
Mensagens de erro e avisosstderr (fprintf(stderr, …), perror)
A mensagem de uso (Uso: ./prog ARQUIVO)stderr (aula Argumentos)
Progresso, "carregando…", resumos de um filtrostderr, para não misturar com o resultado
Perguntas de um programa interativo (Nome: )stdout, o costume; o stdout é esvaziado antes de o programa ler o teclado
Dados num arquivo cujo nome o programa conheceUm FILE * do fopen (aula Escrever Arquivos)

E para ler: stdin quando a entrada é "o que vier" (teclado, <, |), que é o jeito dos filtros; fopen quando o programa recebe o nome do arquivo, por exemplo num argumento.


Quando dá errado

O aviso foi parar dentro do arquivo de resultado
$ ./media_printf < notas.txt > resultado.txt
$ cat resultado.txt
linha 3 ignorada: "sete"
3 notas, média 8.17

O que significa: esta versão do media escreve o aviso com printf, que vai para o stdout, junto com o resultado. Na tela nada apareceu, e o arquivo ficou "sujo".

Como consertar: erros e avisos com fprintf(stderr, …) (a missão da seção 3).

Redirecionei a saída e o arquivo ficou vazio (o programa travou no meio)
#include <stdio.h>

int main(void) {
    int *estoque = NULL;
    printf("Relatório do dia\n");
    printf("Pães vendidos: 42\n");
    printf("Estoque: %d\n", *estoque);
    return 0;
}
$ ./relatorio
Relatório do dia
Pães vendidos: 42
Falha de segmentação     (imagem do núcleo gravada) ./relatorio
$ ./relatorio > relatorio.txt
Falha de segmentação     (imagem do núcleo gravada) ./relatorio > relatorio.txt
$ wc -c relatorio.txt
0 relatorio.txt

O que significa: no terminal, as duas primeiras linhas saíram a cada \n. No arquivo, elas estavam no buffer cheio quando o programa morreu (o *estoque lê um ponteiro nulo; aula Ponteiro Nulo), e se perderam. Parece que o programa nem começou, mas ele morreu na terceira linha.

Como consertar: o erro de verdade é o ponteiro nulo. Para investigar, escreva as mensagens de rastreio no stderr (que não tem buffer) ou chame fflush(stdout); depois delas.

Usei 2>&1 > arquivo e os erros continuaram na tela
$ ./canais 2>&1 > tudo.txt
3. fprintf(stderr, ...)
4. fputs(..., stderr)
$ cat tudo.txt
1. printf
2. fprintf(stdout, ...)

O que significa: o shell lê da esquerda para a direita. O 2>&1 disse "o 2 vai para onde o 1 está agora", e naquele momento o 1 ainda era a tela. Só depois o > tudo.txt mudou o 1.

Como consertar: > tudo.txt 2>&1, nessa ordem, ou &> tudo.txt.

error: passing argument 1 of ‘fprintf’ from incompatible pointer type
$ gcc -std=c23 -Wall -Wextra semfluxo.c -o semfluxo
semfluxo.c: In function ‘main’:
semfluxo.c:4:13: error: passing argument 1 of ‘fprintf’ from incompatible pointer type [-Wincompatible-pointer-types]
    4 |     fprintf("Erro: arquivo não encontrado\n");
      |             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      |             |
      |             char *
In file included from semfluxo.c:1:
/usr/include/stdio.h:364:38: note: expected ‘FILE * restrict’ but argument is of type ‘char *’
  364 | extern int fprintf (FILE *__restrict __stream,
      |                     ~~~~~~~~~~~~~~~~~^~~~~~~~
semfluxo.c:4:5: error: too few arguments to function ‘fprintf’; expected at least 2, have 1
    4 |     fprintf("Erro: arquivo não encontrado\n");
      |     ^~~~~~~
/usr/include/stdio.h:364:12: note: declared here
  364 | extern int fprintf (FILE *__restrict __stream,
      |            ^~~~~~~

O que significa: faltou dizer onde escrever. O primeiro argumento do fprintf tem de ser um FILE * (o canal), e veio o texto.

Como consertar: fprintf(stderr, "Erro: arquivo não encontrado\n");.

feijão virou FEIJãO
$ echo "feijão" | ./maiusculas
FEIJãO

O que significa: no terminal da Ana (UTF-8), o ã ocupa dois char, e o toupper olha um char por vez: ele só conhece as letras sem acento. O editor do guia dá o mesmo resultado.

Como consertar: para texto com acentos, é preciso trabalhar com caracteres "largos" (wchar_t e towupper, com setlocale), um assunto à parte. Para códigos e textos sem acento, o toupper basta.


Referência rápida

EscritaO que faz
stdin, stdout, stderrOs três FILE * abertos pelo sistema (stdio.h); descritores 0, 1 e 2
printf(…) = fprintf(stdout, …)Escreve no stdout
fprintf(stderr, "…", …)Escreve no stderr, com formato
fputs("…", stderr)Escreve um texto (o canal vem depois)
puts("…")Escreve no stdout e acrescenta o \n
getchar() / putchar(c)Um caractere do stdin / para o stdout
fgets(linha, sizeof linha, stdin)Uma linha do stdin
fflush(stdout);Escreve agora o que está no buffer
setvbuf(stdout, NULL, _IONBF, 0);Desliga o buffer do stdout (raro; chame antes de escrever)
>, >>, 2>, &>, > arq 2>&1(shell) Mandam o stdout e/ou o stderr para um arquivo
< arq, prog1 | prog2(shell) O stdin vem de um arquivo / da saída de outro programa
/dev/null(Linux) O "arquivo" que engole a saída e, como entrada, já está no fim

Exercícios

1. O canal dos erros

Complete para escrever a mensagem no canal de erros:

fprintf(, "Arquivo não encontrado\n");

2. O número do stderr

Complete o comando do terminal que joga fora só as mensagens de erro:

./media < notas.txt > /dev/null

3. Descarga

Qual função escreve agora o que está no buffer do stdout?

(stdout);

4. A Ana roda ./media < notas.txt > resultado.txt. Onde aparece o que o programa escreve com fprintf(stderr, …)?

5. Por que, em ./canais > tudo.txt 2>&1, as linhas do stderr ficaram antes das do stdout?

6. Em cat notas.txt | ./media, de onde o media lê?

Missão final: um filtro bem-comportado

Missão: complete o filtro: ele deve escrever no stdout o texto em maiúsculas (com toupper) e contar as linhas (cada '\n' é uma). No fim, mostre o resumo 3 linhas no stderr, para que um ./filtro < lista.txt > LISTA.txt não ponha o resumo dentro do arquivo. A lista de compras já está na Entrada. Saída esperada (o resumo, em vermelho):

ARROZ
LEITE
OVOS
3 linhas
#include <ctype.h>
#include <stdio.h>

int main(void) {
    int c;
    int linhas = 0;
    while ((c = getchar()) != EOF) {
        // conte as linhas e escreva a letra em maiúscula
        putchar(c);
    }
    // mostre o resumo no stderr
    return 0;
}
#include <ctype.h>
#include <stdio.h>

int main(void) {
    int c;
    int linhas = 0;
    while ((c = getchar()) != EOF) {
        if (c == '\n') {
            linhas++;
        }
        putchar(toupper(c));
    }
    fprintf(stderr, "%d linhas\n", linhas);
    return 0;
}

No laboratório, com o resultado num arquivo, o resumo fica na tela e o arquivo sai limpo:

$ gcc -std=c23 -Wall -Wextra filtro.c -o filtro
$ ./filtro < lista.txt > LISTA.txt
3 linhas
$ cat LISTA.txt
ARROZ
LEITE
OVOS

Resumo

  • Todo programa nasce com três canais abertos: stdin (0, o teclado), stdout (1, a tela) e stderr (2, a tela).
  • printf escreve no stdout; erros, avisos e mensagens de uso vão para o stderr, com fprintf(stderr, …) ou perror.
  • O shell liga os canais antes de o programa começar: > e >> (stdout), 2> (stderr), > arq 2>&1 ou &> (os dois), < (stdin) e | (um programa no outro).
  • Um filtro lê o stdin e escreve o resultado no stdout, e só o resultado.
  • O stdout tem buffer: de linha no terminal, cheio em arquivos e canos. O stderr escreve na hora. Por isso a ordem pode mudar; fflush(stdout) escreve na hora. Se o programa morre, o buffer se perde.