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.
- O que são
stdin,stdoutestderr, e os números 0, 1 e 2. - Por que
printf(…)é o mesmo quefprintf(stdout, …), e quando usarfprintf(stderr, …). - Como o terminal redireciona os canais:
>,>>,2>,2>&1,<,|e/dev/null. - Como escrever um filtro: um programa que lê o
stdine escreve nostdout. - O que é o buffer da saída, por que ele troca a ordem das linhas e como o
fflushresolve.
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
fprintfda aula Escrever Arquivos, que recebe onde escrever.stdouté uma variável do tipoFILE *, como a que ofopendevolve, 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).
| Nome | Em inglês | Número | Ligado, por padrão, a | Para quê |
|---|---|---|---|---|
stdin | standard input, entrada padrão | 0 | teclado | O que o programa lê: scanf, fgets(…, stdin), getchar |
stdout | standard output, saída padrão | 1 | tela | O resultado: printf, puts, putchar |
stderr | standard error, saída de erros | 2 | tela | Erros, 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 terminal | O que faz |
|---|---|
prog > arq | stdout vai para arq (apaga o que havia) |
prog >> arq | stdout vai para o fim de arq |
prog 2> arq | stderr vai para arq |
prog > arq 2>&1 | Os dois vão para arq ("o 2 vai para onde o 1 está agora") |
prog &> arq | O mesmo, forma curta do Bash |
prog 2> /dev/null | Joga o stderr fora (/dev/null é um "arquivo" que engole tudo) |
prog < arq | stdin vem de arq (seção 3) |
prog1 | prog2 | O 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
stdinaté 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
continuepula 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.)
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ãochar, para caber oEOF(aula char). - getchar()
- Lê um caractere do
stdin. É o mesmo quegetc(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 queputc(…, 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.
| Canal | Quando escreve de verdade |
|---|---|
stdout ligado ao terminal | A cada \n (buffer de linha), quando o programa vai ler o teclado, ou no fim |
stdout ligado a um arquivo ou cano | Quando junta 4.096 bytes (buffer cheio), ou no fim do programa |
stderr | Na 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 nostderrquando 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).
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 escreve | Canal |
|---|---|
| O resultado: a média, o texto transformado, o relatório | stdout (printf, puts, putchar) |
| Mensagens de erro e avisos | stderr (fprintf(stderr, …), perror) |
A mensagem de uso (Uso: ./prog ARQUIVO) | stderr (aula Argumentos) |
| Progresso, "carregando…", resumos de um filtro | stderr, 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 conhece | Um 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
$ ./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).
#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.
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
| Escrita | O que faz |
|---|---|
stdin, stdout, stderr | Os 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:
2. O número do stderr
Complete o comando do terminal que joga fora só as mensagens de erro:
3. Descarga
Qual função escreve agora o que está no buffer do 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) estderr(2, a tela). printfescreve nostdout; erros, avisos e mensagens de uso vão para ostderr, comfprintf(stderr, …)ouperror.- O shell liga os canais antes de o programa começar:
>e>>(stdout),2>(stderr),> arq 2>&1ou&>(os dois),<(stdin) e|(um programa no outro). - Um filtro lê o
stdine escreve o resultado nostdout, e só o resultado. - O
stdouttem buffer: de linha no terminal, cheio em arquivos e canos. Ostderrescreve na hora. Por isso a ordem pode mudar;fflush(stdout)escreve na hora. Se o programa morre, o buffer se perde.