Projeto Contador de Palavras
A Ana escreve os textos da vitrine da lojinha e das postagens nas redes sociais, e às vezes precisa saber quantas palavras ou letras eles têm. O Linux já tem um programa para isso, o wc (word count, contagem de palavras). Neste projeto você vai escrever o seu próprio wc em C, do zero, e conferir cada versão contra o wc de verdade. No caminho, vai entender como um programa lê um texto letra por letra e por que "café" tem 4 letras e 5 bytes.
- Ler a entrada um caractere por vez com
getcharaté oEOF, e por que a variável tem de serint. - Mandar um arquivo para a entrada do programa com
<e digitar até o Ctrl+D. - Contar palavras com uma variável de estado ("estou dentro de uma palavra?") e
isspace(ctype.h). - Abrir os arquivos que vêm na linha de comando (argc e argv), juntar as contagens numa struct e mostrar o total.
- Separar bytes, caracteres e letras no UTF-8 (Acentos e UTF-8) com uma máscara de bits.
O que vamos construir
O contador pronto, na máquina da Ana, com dois textos dela. Logo abaixo, o wc do Linux com as mesmas colunas: os números batem.
$ ./contar poema.txt lista.txt
Linhas Palavras Letras Caract. Bytes Arquivo
5 24 93 123 126 poema.txt
3 5 23 30 33 lista.txt
8 29 116 153 159 total
$ wc -lwmc poema.txt lista.txt
5 24 123 126 poema.txt
3 5 30 33 lista.txt
8 29 153 159 total
O wc -lwmc mostra linhas (-l), palavras (-w), caracteres (-m) e bytes (-c). O wc não conta letras; o nosso conta.
- Linhas e caracteres com getchar.
- Palavras: dentro ou fora.
- Arquivos pelo nome e o total.
- Letras, acentos e bytes.
Antes de começar
Você precisa ter visto while, Caracteres (char), if e else e, para a versão 3, Argumentos da Linha de Comando e Arquivos (fopen e fclose). A Ana criou a pasta e dois textos de teste com o editor de textos dela. O poema.txt tem uma linha em branco e acentos; a lista.txt tem espaços duplos de propósito:
$ mkdir -p ~/c/texto $ cd ~/c/texto $ cat poema.txt A lojinha da Ana abre às oito. Tem caderno, caneta e borracha, e um gato que dorme no balcão. Quem chega cedo ganha café. $ cat lista.txt arroz feijão e macarrão pão
Versão 1 · Linhas e caracteres com getchar
Pense numa caixa de supermercado que conta moedas: ela pega uma por uma, até o pote ficar vazio. O programa faz igual com o texto: pega um caractere com getchar ("get character", pegar um caractere), soma 1 no contador e repete, até a entrada acabar. Cada vez que o caractere é o Enter ('\n'), uma linha terminou.
#include <stdio.h>
int main(void) {
long linhas = 0;
long caracteres = 0;
int c;
while ((c = getchar()) != EOF) {
caracteres++;
if (c == '\n') {
linhas++;
}
}
printf("Linhas: %ld\n", linhas);
printf("Caracteres: %ld\n", caracteres);
return 0;
}
| Linha | O que faz |
|---|---|
long linhas = 0; | Os contadores começam em zero. São long porque um texto pode ser grande (no gcc do laboratório, um long vai até 9 quintilhões; no editor do guia, até 2 bilhões, o que também sobra). |
int c; | Guarda o caractere lido. É int, e não char, de propósito: o getchar devolve um byte (de 0 a 255) ou o valor especial EOF, e precisa de um tipo que caiba os 257 valores (veja o cenário abaixo). |
while ((c = getchar()) != EOF) | Três coisas numa linha: lê um caractere, guarda em c e compara com EOF ("end of file", fim do arquivo). Os parênteses em volta de c = getchar() são obrigatórios: sem eles, o != seria feito primeiro (aula Precedência). |
caracteres++; | Todo caractere conta, inclusive espaços e Enters. |
if (c == '\n') linhas++; | Uma linha termina no Enter. Contar linhas é contar '\n'. |
Há dois jeitos de dar um texto para o programa. O primeiro é mandar um arquivo para a entrada com <: o terminal abre o arquivo e o getchar lê dele, como se alguém digitasse muito rápido (aula stdin, stdout e stderr). O wc confirma as contas:
$ gcc -std=c23 -Wall -Wextra contar.c -o contar $ ./contar < poema.txt Linhas: 5 Caracteres: 126 $ wc -l -c poema.txt 5 126 poema.txt
O segundo é digitar. O programa espera; você escreve as linhas e, no fim, aperta Ctrl+D numa linha vazia, que no terminal do Linux quer dizer "a entrada acabou". Aí o getchar devolve EOF e o laço termina:
$ ./contar Olá, mundo! Tchau. « depois do Enter, a Ana apertou Ctrl+D Linhas: 2 Caracteres: 20
20 caracteres? Olá, mundo! tem 11 e Tchau. tem 6, mais os dois Enters: 19. O que sobrou é o á, que ocupa 2 bytes no UTF-8, e o getchar lê bytes. Por enquanto, o que chamamos de "caracteres" é na verdade "bytes", como o wc -c. A versão 4 conserta o nome e a conta.
Se o arquivo não termina com Enter, a última linha não tem '\n', e nem o nosso programa nem o wc a contam:
$ printf 'um\ndois\ntrês' > sem-enter.txt $ cat sem-enter.txt um dois três$ ./contar < sem-enter.txt « o $ grudou: não há Enter depois de "três" Linhas: 2 Caracteres: 13 $ wc -l sem-enter.txt 2 sem-enter.txt
Não é defeito: no Linux, uma "linha" é por definição um texto que termina em '\n', e o wc -l conta os '\n'. Por isso os editores de código põem um Enter no fim do arquivo. Contar também a última linha incompleta é um dos desafios.
char c: o programa para no meio do arquivo
A Ana escreveu char c; em vez de int c;. Compilou sem nenhum aviso, e com o poema funcionou. Depois ela testou com um arquivo que não é texto, o próprio programa ls do Linux:
$ gcc -std=c23 -Wall -Wextra contar.c -o contar $ ./contar < poema.txt Linhas: 5 Caracteres: 126 $ ./contar < /usr/bin/ls Linhas: 3 Caracteres: 8288 $ wc -c /usr/bin/ls 150568 /usr/bin/ls $ xxd -s 8284 -l 8 /usr/bin/ls 0000205c: 0000 0000 ffd2 0100 ........
O que aconteceu: o programa parou depois de 8288 bytes, de um arquivo de 150568. O xxd mostra o byte seguinte, o de posição 8288 (0x2060): é ff, o 255. O EOF vale -1, e um char com sinal (o padrão no gcc e no editor) só guarda de -128 a 127: o byte 255 vira -1 ao entrar em c, e o programa acha que o arquivo acabou. Num texto em português o byte 255 não aparece (no UTF-8 ele nunca é usado), por isso o erro passou despercebido com o poema.
Como consertar: int c;. Com int, o byte 255 é 255 e o EOF é -1: dois valores diferentes. Vale para getchar, getc e fgetc, que devolvem int pelo mesmo motivo (aula Caracteres).
Missão (versão 1): o contador conta os caracteres, mas as linhas saem 0. No lugar do comentário, conte uma linha cada vez que c for o Enter ('\n'). A entrada da missão é o poema da Ana: devem sair Linhas: 5 e Caracteres: 126.
#include <stdio.h>
int main(void) {
long linhas = 0;
long caracteres = 0;
int c;
while ((c = getchar()) != EOF) {
caracteres++;
// se c for o Enter ('\n'), conte mais uma linha
}
printf("Linhas: %ld\n", linhas);
printf("Caracteres: %ld\n", caracteres);
return 0;
}
#include <stdio.h>
int main(void) {
long linhas = 0;
long caracteres = 0;
int c;
while ((c = getchar()) != EOF) {
caracteres++;
if (c == '\n') {
linhas++;
}
}
printf("Linhas: %ld\n", linhas);
printf("Caracteres: %ld\n", caracteres);
return 0;
}
Versão 2 · Palavras: dentro ou fora
Contar palavras parece fácil: "conte os espaços". Mas entre duas palavras pode haver dois espaços, um Tab ou uma linha em branco. Um jeito que nunca erra: imagine o porteiro de uma loja, que conta quem entra. Ele não conta os passos de cada cliente, só o momento em que alguém passa da rua para dentro. O programa guarda numa variável se está dentro de uma palavra ou fora (num espaço); cada vez que passa de fora para dentro, começou uma palavra nova.
texto: feijão e macarrão
estado: DDDDDDFFDFFDDDDDDDD « D = dentro, F = fora
↑ ↑ ↑
1 2 3 « conta só quando passa de F para D
#include <stdio.h>
#include <ctype.h>
int main(void) {
long linhas = 0;
long palavras = 0;
long caracteres = 0;
bool dentro = false; // estou no meio de uma palavra?
int c;
while ((c = getchar()) != EOF) {
caracteres++;
if (c == '\n') {
linhas++;
}
if (isspace(c)) {
dentro = false; // espaço, Tab ou Enter: a palavra acabou
} else if (!dentro) {
dentro = true; // primeira letra de uma palavra nova
palavras++;
}
}
printf("Linhas: %ld\n", linhas);
printf("Palavras: %ld\n", palavras);
printf("Caracteres: %ld\n", caracteres);
return 0;
}
- #include <ctype.h>
- A biblioteca que classifica caracteres (aula ctype.h).
- bool dentro = false;
- O estado: começa fora de palavra. No C23,
bool,trueefalsejá vêm na linguagem C23; antes era preciso#include <stdbool.h>(aula Booleanos). - isspace(c)
- Verdadeiro para os "espaços em branco": o espaço, o Tab (
'\t'), o Enter ('\n') e alguns outros ('\r','\v','\f'). Owcsepara as palavras do mesmo jeito. - dentro = false;
- Achou um espaço: a palavra acabou (ou já estava fora, e continua fora).
- else if (!dentro)
- Não é espaço e estava fora: é a primeira letra de uma palavra. Marca
dentro = truee conta. As letras seguintes da mesma palavra caem noelse ifcomdentrojá verdadeiro, e não contam de novo.
$ gcc -std=c23 -Wall -Wextra contar.c -o contar $ ./contar < poema.txt Linhas: 5 Palavras: 24 Caracteres: 126 $ wc poema.txt 5 24 126 poema.txt $ ./contar < lista.txt Linhas: 3 Palavras: 5 Caracteres: 33 $ wc lista.txt 3 5 33 lista.txt $ printf 'um\tdois três\n\n\n quatro\n' | ./contar Linhas: 4 Palavras: 4 Caracteres: 27
Sem argumentos, o wc mostra as três contagens nesta ordem: linhas, palavras e bytes. Ele também ajusta a largura das colunas ao tamanho dos arquivos; o que importa são os números. O último teste, com o printf … | (o | manda a saída de um comando para a entrada do outro), tem um Tab, três espaços seguidos, duas linhas em branco e espaços no começo da linha: são 4 palavras, e o programa não se confunde. Repare também que "caderno," é uma palavra: para o wc e para nós, palavra é tudo o que fica entre espaços, pontuação inclusa.
A primeira ideia da Ana foi "toda palavra termina num espaço ou num Enter; basta contar os dois":
if (c == ' ' || c == '\n') {
palavras++; // toda palavra termina num espaço ou num Enter
}
$ gcc -std=c23 -Wall -Wextra contar.c -o contar $ ./contar < poema.txt Linhas: 5 Palavras: 25 « o wc diz 24 Caracteres: 126 $ ./contar < lista.txt Linhas: 3 Palavras: 7 « o wc diz 5 Caracteres: 33
O que aconteceu: a linha em branco do poema é um Enter sem palavra antes, e contou como palavra. Na lista, cada espaço duplo de feijão e macarrão conta duas vezes. O programa contou separadores, e o número de separadores só é igual ao de palavras quando o texto é "bem comportado". Um Tab no lugar do espaço ainda grudaria duas palavras numa.
Como consertar: conte as passagens de fora para dentro de uma palavra, com a variável dentro, e use isspace para reconhecer todos os tipos de espaço.
Missão (versão 2): as palavras saem 0. No lugar dos dois comentários, escreva o if com isspace(c) e o else if (!dentro) que conta uma palavra nova. A entrada da missão é a lista da Ana, com os espaços duplos: devem sair Palavras: 5 e Linhas: 3.
#include <stdio.h>
#include <ctype.h>
int main(void) {
long linhas = 0;
long palavras = 0;
long caracteres = 0;
bool dentro = false; // estou no meio de uma palavra?
int c;
while ((c = getchar()) != EOF) {
caracteres++;
if (c == '\n') {
linhas++;
}
// se c for espaço (isspace), a palavra acabou: dentro = false
// senão, se ainda não estava dentro, começou uma palavra nova
}
printf("Linhas: %ld\n", linhas);
printf("Palavras: %ld\n", palavras);
printf("Caracteres: %ld\n", caracteres);
return 0;
}
#include <stdio.h>
#include <ctype.h>
int main(void) {
long linhas = 0;
long palavras = 0;
long caracteres = 0;
bool dentro = false; // estou no meio de uma palavra?
int c;
while ((c = getchar()) != EOF) {
caracteres++;
if (c == '\n') {
linhas++;
}
if (isspace(c)) {
dentro = false; // espaço, Tab ou Enter: a palavra acabou
} else if (!dentro) {
dentro = true; // primeira letra de uma palavra nova
palavras++;
}
}
printf("Linhas: %ld\n", linhas);
printf("Palavras: %ld\n", palavras);
printf("Caracteres: %ld\n", caracteres);
return 0;
}
Versão 3 · Arquivos pelo nome e o total
O wc de verdade recebe os nomes dos arquivos na linha de comando (wc poema.txt lista.txt), mostra uma linha para cada um e, se forem vários, o total. Para isso, a versão 3 tem três novidades:
- a contagem vira uma função,
contar(FILE *f), que lê de qualquer arquivo aberto, inclusive do teclado; - as três contagens viajam juntas numa struct,
Contagem; - o
mainrecebe os nomes emargve abre um arquivo por vez.
#include <stdio.h>
#include <ctype.h>
typedef struct {
long linhas;
long palavras;
long caracteres;
} Contagem;
// Conta tudo o que der para ler de f, até o fim do arquivo.
Contagem contar(FILE *f) {
Contagem t = {0};
bool dentro = false;
int c;
while ((c = getc(f)) != EOF) {
t.caracteres++;
if (c == '\n') {
t.linhas++;
}
if (isspace(c)) {
dentro = false;
} else if (!dentro) {
dentro = true;
t.palavras++;
}
}
return t;
}
void mostrar(Contagem t, const char *nome) {
printf("%7ld %8ld %7ld %s\n", t.linhas, t.palavras, t.caracteres, nome);
}
int main(int argc, char *argv[]) {
printf(" Linhas Palavras Caract. Arquivo\n");
if (argc == 1) { // sem nomes: lê o teclado (stdin)
mostrar(contar(stdin), "-");
return 0;
}
Contagem total = {0};
int erros = 0;
for (int i = 1; i < argc; i++) {
FILE *f = fopen(argv[i], "r");
if (f == NULL) {
perror(argv[i]);
erros++;
continue; // pula para o próximo arquivo
}
Contagem t = contar(f);
fclose(f);
mostrar(t, argv[i]);
total.linhas += t.linhas;
total.palavras += t.palavras;
total.caracteres += t.caracteres;
}
if (argc > 2) {
mostrar(total, "total");
}
return erros > 0 ? 1 : 0;
}
| Linha | O que faz |
|---|---|
typedef struct { … } Contagem; | As três contagens de um texto numa ficha só (aula Structs). A função devolve a ficha inteira de uma vez. |
Contagem t = {0}; | Todos os campos começam em zero. |
getc(f) | O mesmo que o getchar, mas do arquivo f. Na verdade, getchar() é o mesmo que getc(stdin). |
printf("%7ld %8ld %7ld %s\n", …) | Cada número numa coluna de largura fixa, alinhado à direita, embaixo do título (aula Formatos do printf). |
int main(int argc, char *argv[]) | argc é quantas palavras há na linha de comando, contando o próprio programa; argv[1], argv[2]… são os nomes dos arquivos (aula Argumentos da Linha de Comando). |
if (argc == 1) mostrar(contar(stdin), "-"); | Sem nomes, lê a entrada padrão (stdin), que também é um FILE *, como o wc faz. O "-" no lugar do nome é a convenção do Linux para "entrada padrão". |
perror(argv[i]); erros++; continue; | O arquivo não abriu: mostra o motivo (aula errno e perror), anota o erro e pula para o próximo nome. Um arquivo faltando não impede de contar os outros. |
fclose(f); | Fecha cada arquivo assim que termina de contar. Um programa que conta mil arquivos sem fechar nenhum esgota o limite de arquivos abertos do sistema. |
total.linhas += t.linhas; | Acumula a contagem de cada arquivo no total. |
if (argc > 2) | Só mostra o total quando há mais de um arquivo, como o wc. |
return erros > 0 ? 1 : 0; | Código de saída 1 se algum arquivo falhou, como o wc (aula Operador Ternário). Um script que use o contador fica sabendo que algo deu errado. |
$ gcc -std=c23 -Wall -Wextra contar.c -o contar
$ ./contar poema.txt
Linhas Palavras Caract. Arquivo
5 24 126 poema.txt
$ ./contar poema.txt lista.txt
Linhas Palavras Caract. Arquivo
5 24 126 poema.txt
3 5 33 lista.txt
8 29 159 total
$ wc poema.txt lista.txt
5 24 126 poema.txt
3 5 33 lista.txt
8 29 159 total
$ ./contar < lista.txt
Linhas Palavras Caract. Arquivo
3 5 33 -
E com um arquivo que não existe no meio? Os dois avisam, contam os outros e saem com o código 1:
$ ./contar poema.txt nao-existe.txt lista.txt; echo $?
Linhas Palavras Caract. Arquivo
5 24 126 poema.txt
nao-existe.txt: No such file or directory
3 5 33 lista.txt
8 29 159 total
1
$ wc poema.txt nao-existe.txt lista.txt; echo $?
5 24 126 poema.txt
wc: nao-existe.txt: Arquivo ou diretório inexistente
3 5 33 lista.txt
8 29 159 total
1
A única diferença é a língua do erro. O wc chama setlocale ao começar e usa o idioma do sistema da Ana, o português; o nosso programa não chama, e o perror fala inglês, o padrão do C. O editor do guia também mostra No such file or directory.
Falha de segmentação quando falta um arquivo
Numa primeira versão, a Ana esqueceu o continue depois do perror. O gcc não avisou nada:
$ gcc -std=c23 -Wall -Wextra contar.c -o contar
$ ./contar poema.txt nao-existe.txt lista.txt
Linhas Palavras Caract. Arquivo
5 24 126 poema.txt
nao-existe.txt: No such file or directory
Falha de segmentação (imagem do núcleo gravada) ./contar poema.txt nao-existe.txt lista.txt
$ echo $?
139
O que aconteceu: o perror mostrou o erro, mas o programa seguiu em frente e chamou contar(f) com f valendo NULL. O getc(NULL) tentou ler da memória do endereço zero, e o sistema matou o programa (código 139; veja Ponteiro Nulo). A lista.txt nem chegou a ser contada.
Como consertar: depois de tratar o erro, saia do caminho normal: continue (pula para o próximo arquivo, dentro de um laço) ou return. Conferir o NULL só adianta se o programa fizer alguma coisa diferente quando ele aparece.
Missão (versão 3): o programa conta os dois arquivos das abas, mas a linha total sai zerada. No lugar do comentário, some t em total, campo por campo (total.linhas += t.linhas; e os outros dois). O total deve ser 8 29 159.
#include <stdio.h>
#include <ctype.h>
typedef struct {
long linhas;
long palavras;
long caracteres;
} Contagem;
// Conta tudo o que der para ler de f, até o fim do arquivo.
Contagem contar(FILE *f) {
Contagem t = {0};
bool dentro = false;
int c;
while ((c = getc(f)) != EOF) {
t.caracteres++;
if (c == '\n') {
t.linhas++;
}
if (isspace(c)) {
dentro = false;
} else if (!dentro) {
dentro = true;
t.palavras++;
}
}
return t;
}
void mostrar(Contagem t, const char *nome) {
printf("%7ld %8ld %7ld %s\n", t.linhas, t.palavras, t.caracteres, nome);
}
int main(int argc, char *argv[]) {
printf(" Linhas Palavras Caract. Arquivo\n");
if (argc == 1) { // sem nomes: lê o teclado (stdin)
mostrar(contar(stdin), "-");
return 0;
}
Contagem total = {0};
int erros = 0;
for (int i = 1; i < argc; i++) {
FILE *f = fopen(argv[i], "r");
if (f == NULL) {
perror(argv[i]);
erros++;
continue; // pula para o próximo arquivo
}
Contagem t = contar(f);
fclose(f);
mostrar(t, argv[i]);
// some t em total, campo por campo
}
if (argc > 2) {
mostrar(total, "total");
}
return erros > 0 ? 1 : 0;
}
A lojinha da Ana abre às oito. Tem caderno, caneta e borracha, e um gato que dorme no balcão. Quem chega cedo ganha café.
arroz feijão e macarrão pão
#include <stdio.h>
#include <ctype.h>
typedef struct {
long linhas;
long palavras;
long caracteres;
} Contagem;
// Conta tudo o que der para ler de f, até o fim do arquivo.
Contagem contar(FILE *f) {
Contagem t = {0};
bool dentro = false;
int c;
while ((c = getc(f)) != EOF) {
t.caracteres++;
if (c == '\n') {
t.linhas++;
}
if (isspace(c)) {
dentro = false;
} else if (!dentro) {
dentro = true;
t.palavras++;
}
}
return t;
}
void mostrar(Contagem t, const char *nome) {
printf("%7ld %8ld %7ld %s\n", t.linhas, t.palavras, t.caracteres, nome);
}
int main(int argc, char *argv[]) {
printf(" Linhas Palavras Caract. Arquivo\n");
if (argc == 1) { // sem nomes: lê o teclado (stdin)
mostrar(contar(stdin), "-");
return 0;
}
Contagem total = {0};
int erros = 0;
for (int i = 1; i < argc; i++) {
FILE *f = fopen(argv[i], "r");
if (f == NULL) {
perror(argv[i]);
erros++;
continue; // pula para o próximo arquivo
}
Contagem t = contar(f);
fclose(f);
mostrar(t, argv[i]);
total.linhas += t.linhas;
total.palavras += t.palavras;
total.caracteres += t.caracteres;
}
if (argc > 2) {
mostrar(total, "total");
}
return erros > 0 ? 1 : 0;
}
Versão 4 · Letras, acentos e bytes
Na versão 1 ficou uma pendência: Olá, mundo! deu um "caractere" a mais. É que o getc lê bytes, e no UTF-8, a codificação de texto do Linux e da internet, as letras acentuadas ocupam 2 bytes (aula Acentos e UTF-8). Veja os bytes de "café":
$ echo -n 'café' | xxd 00000000: 6361 66c3 a9 caf.. $ echo -n 'café' | wc -m 4 $ echo -n 'café' | LANG=C wc -m 5
São 5 bytes: 63 (c), 61 (a), 66 (f) e o par c3 a9, que juntos formam o é. O wc -m conta 4 caracteres, porque sabe que o sistema da Ana está em UTF-8 (pt_BR.UTF-8); com LANG=C, ele passa a contar bytes e diz 5. A versão 4 separa as três ideias:
| Contagem | "café" | O que conta | Igual a |
|---|---|---|---|
| bytes | 5 | cada byte do arquivo | wc -c |
| caracteres | 4 | cada símbolo que aparece na tela, inclusive espaços e pontuação | wc -m |
| letras | 4 | só as letras, com ou sem acento | (o wc não tem) |
Como achar o começo de cada caractere
O UTF-8 foi desenhado para isso ser fácil. Os bytes do meio de um caractere (os que continuam um caractere já começado, como o a9 do é) sempre começam com os bits 10. Nenhum outro byte começa assim:
| Byte | Em binário | O que é |
|---|---|---|
63 (c) | 0110 0011 | começa com 0: um caractere inteiro de 1 byte (ASCII) |
c3 | 1100 0011 | começa com 11: o primeiro byte de um caractere de 2 bytes ou mais |
a9 | 1010 1001 | começa com 10: continuação; não é um caractere novo |
Para olhar só os dois primeiros bits, a máscara 0xC0 (1100 0000) apaga os outros seis (aula Máscaras e Flags): c & 0xC0 dá 0x80 (1000 0000) exatamente para os bytes de continuação. Então: caracteres = bytes que não são continuação.
E as letras? As do inglês, o isalpha reconhece. As acentuadas do português (á à â ã é ê í ó ô õ ú ç e as maiúsculas) começam todas pelo byte 0xC3 no UTF-8. Contamos uma letra para cada 0xC3, e a continuação dele não conta de novo.
#include <stdio.h>
#include <ctype.h>
typedef struct {
long linhas;
long palavras;
long letras;
long caracteres; // como o wc -m: "ã" conta uma vez
long bytes; // como o wc -c: "ã" conta duas
} Contagem;
// No UTF-8, os bytes de 0x80 a 0xBF (10xxxxxx) continuam uma letra
// que começou antes. Todos os outros começam um caractere novo.
bool continua_caractere(int c) {
return (c & 0xC0) == 0x80;
}
// As letras acentuadas do português (á, ã, ç, é, ô, ú...) começam
// todas pelo byte 0xC3 no UTF-8.
bool comeca_letra(int c) {
return isalpha(c) || c == 0xC3;
}
Contagem contar(FILE *f) {
Contagem t = {0};
bool dentro = false;
int c;
while ((c = getc(f)) != EOF) {
t.bytes++;
if (!continua_caractere(c)) {
t.caracteres++;
}
if (comeca_letra(c)) {
t.letras++;
}
if (c == '\n') {
t.linhas++;
}
if (isspace(c)) {
dentro = false;
} else if (!dentro) {
dentro = true;
t.palavras++;
}
}
return t;
}
void mostrar(Contagem t, const char *nome) {
printf("%7ld %8ld %7ld %8ld %7ld %s\n", t.linhas, t.palavras,
t.letras, t.caracteres, t.bytes, nome);
}
void somar(Contagem *total, Contagem t) {
total->linhas += t.linhas;
total->palavras += t.palavras;
total->letras += t.letras;
total->caracteres += t.caracteres;
total->bytes += t.bytes;
}
int main(int argc, char *argv[]) {
printf(" Linhas Palavras Letras Caract. Bytes Arquivo\n");
if (argc == 1) {
mostrar(contar(stdin), "-");
return 0;
}
Contagem total = {0};
int erros = 0;
for (int i = 1; i < argc; i++) {
FILE *f = fopen(argv[i], "r");
if (f == NULL) {
perror(argv[i]);
erros++;
continue;
}
Contagem t = contar(f);
fclose(f);
mostrar(t, argv[i]);
somar(&total, t);
}
if (argc > 2) {
mostrar(total, "total");
}
return erros > 0 ? 1 : 0;
}
- long letras; long caracteres; long bytes;
- A struct ganhou dois campos; o antigo
caracteres, que contava bytes, agora se chamabytes. - bool continua_caractere(int c)
(c & 0xC0) == 0x80: verdadeiro só para os bytes10xxxxxx. Com uma função de nome claro, oiflá embaixo se lê como uma frase: "se não continua um caractere, conte um caractere".- bool comeca_letra(int c)
- Uma letra sem acento (
isalpha, que no padrão do C só reconhece A a Z e a a z) ou o primeiro byte de uma letra acentuada (0xC3). - void somar(Contagem *total, Contagem t)
- Com cinco campos, as somas do
mainviraram uma função. Ela recebe o endereço do total para poder mudá-lo, e usa->para chegar nos campos (aula Structs e Ponteiros).
$ gcc -std=c23 -Wall -Wextra contar.c -o contar
$ ./contar poema.txt lista.txt
Linhas Palavras Letras Caract. Bytes Arquivo
5 24 93 123 126 poema.txt
3 5 23 30 33 lista.txt
8 29 116 153 159 total
$ wc -lwmc poema.txt lista.txt
5 24 123 126 poema.txt
3 5 30 33 lista.txt
8 29 153 159 total
O poema tem 3 letras acentuadas (às, balcão, café): 126 bytes − 3 = 123 caracteres, como o wc -m. A lista tem 3 também (feijão, macarrão, pão). E as letras? O wc não conta, mas o grep sabe separar letras: grep -o '[[:alpha:]]' mostra cada letra numa linha, e o wc -l conta as linhas.
$ grep -o '[[:alpha:]]' poema.txt | wc -l 93
0xC3
O teste c == 0xC3 cobre as letras acentuadas do português, do espanhol e do francês. Letras de outras línguas (o ł do polonês, o grego, o russo) começam por outros bytes e não seriam contadas; e o 0xC3 também começa dois símbolos que não são letras, o × e o ÷. Para um contador que entenda qualquer língua, a saída é a biblioteca de caracteres "largos" do C (<wchar.h> e <wctype.h>, com setlocale), que fica para depois.
A primeira ideia da Ana para as letras acentuadas foi "todo byte acima de 127 é parte de uma letra": return isalpha(c) || c >= 0x80;.
$ gcc -std=c23 -Wall -Wextra contar.c -o contar
$ ./contar poema.txt lista.txt
Linhas Palavras Letras Caract. Bytes Arquivo
5 24 96 123 126 poema.txt « eram 93
3 5 26 30 33 lista.txt « eram 23
8 29 122 153 159 total
O que aconteceu: cada letra acentuada tem dois bytes acima de 127 (c3 e a continuação), e as duas contaram. O poema e a lista têm 3 acentos cada um: 3 a mais em cada.
Como consertar: conte só o primeiro byte da letra (0xC3), nunca a continuação. A mesma ideia vale para os caracteres: o que conta é o começo, e não cada pedaço.
Missão (versão 4): a coluna Caract. está igual à coluna Bytes, porque o programa conta todo byte como caractere. Use a função continua_caractere para contar só os bytes que começam um caractere. O poema deve dar 123 caracteres e a lista 30, como o wc -m.
#include <stdio.h>
#include <ctype.h>
typedef struct {
long linhas;
long palavras;
long letras;
long caracteres; // como o wc -m: "ã" conta uma vez
long bytes; // como o wc -c: "ã" conta duas
} Contagem;
// No UTF-8, os bytes de 0x80 a 0xBF (10xxxxxx) continuam uma letra
// que começou antes. Todos os outros começam um caractere novo.
bool continua_caractere(int c) {
return (c & 0xC0) == 0x80;
}
// As letras acentuadas do português (á, ã, ç, é, ô, ú...) começam
// todas pelo byte 0xC3 no UTF-8.
bool comeca_letra(int c) {
return isalpha(c) || c == 0xC3;
}
Contagem contar(FILE *f) {
Contagem t = {0};
bool dentro = false;
int c;
while ((c = getc(f)) != EOF) {
t.bytes++;
t.caracteres++; // conta todo byte: está igual a t.bytes
if (comeca_letra(c)) {
t.letras++;
}
if (c == '\n') {
t.linhas++;
}
if (isspace(c)) {
dentro = false;
} else if (!dentro) {
dentro = true;
t.palavras++;
}
}
return t;
}
void mostrar(Contagem t, const char *nome) {
printf("%7ld %8ld %7ld %8ld %7ld %s\n", t.linhas, t.palavras,
t.letras, t.caracteres, t.bytes, nome);
}
void somar(Contagem *total, Contagem t) {
total->linhas += t.linhas;
total->palavras += t.palavras;
total->letras += t.letras;
total->caracteres += t.caracteres;
total->bytes += t.bytes;
}
int main(int argc, char *argv[]) {
printf(" Linhas Palavras Letras Caract. Bytes Arquivo\n");
if (argc == 1) {
mostrar(contar(stdin), "-");
return 0;
}
Contagem total = {0};
int erros = 0;
for (int i = 1; i < argc; i++) {
FILE *f = fopen(argv[i], "r");
if (f == NULL) {
perror(argv[i]);
erros++;
continue;
}
Contagem t = contar(f);
fclose(f);
mostrar(t, argv[i]);
somar(&total, t);
}
if (argc > 2) {
mostrar(total, "total");
}
return erros > 0 ? 1 : 0;
}
A lojinha da Ana abre às oito. Tem caderno, caneta e borracha, e um gato que dorme no balcão. Quem chega cedo ganha café.
arroz feijão e macarrão pão
#include <stdio.h>
#include <ctype.h>
typedef struct {
long linhas;
long palavras;
long letras;
long caracteres; // como o wc -m: "ã" conta uma vez
long bytes; // como o wc -c: "ã" conta duas
} Contagem;
// No UTF-8, os bytes de 0x80 a 0xBF (10xxxxxx) continuam uma letra
// que começou antes. Todos os outros começam um caractere novo.
bool continua_caractere(int c) {
return (c & 0xC0) == 0x80;
}
// As letras acentuadas do português (á, ã, ç, é, ô, ú...) começam
// todas pelo byte 0xC3 no UTF-8.
bool comeca_letra(int c) {
return isalpha(c) || c == 0xC3;
}
Contagem contar(FILE *f) {
Contagem t = {0};
bool dentro = false;
int c;
while ((c = getc(f)) != EOF) {
t.bytes++;
if (!continua_caractere(c)) {
t.caracteres++;
}
if (comeca_letra(c)) {
t.letras++;
}
if (c == '\n') {
t.linhas++;
}
if (isspace(c)) {
dentro = false;
} else if (!dentro) {
dentro = true;
t.palavras++;
}
}
return t;
}
void mostrar(Contagem t, const char *nome) {
printf("%7ld %8ld %7ld %8ld %7ld %s\n", t.linhas, t.palavras,
t.letras, t.caracteres, t.bytes, nome);
}
void somar(Contagem *total, Contagem t) {
total->linhas += t.linhas;
total->palavras += t.palavras;
total->letras += t.letras;
total->caracteres += t.caracteres;
total->bytes += t.bytes;
}
int main(int argc, char *argv[]) {
printf(" Linhas Palavras Letras Caract. Bytes Arquivo\n");
if (argc == 1) {
mostrar(contar(stdin), "-");
return 0;
}
Contagem total = {0};
int erros = 0;
for (int i = 1; i < argc; i++) {
FILE *f = fopen(argv[i], "r");
if (f == NULL) {
perror(argv[i]);
erros++;
continue;
}
Contagem t = contar(f);
fclose(f);
mostrar(t, argv[i]);
somar(&total, t);
}
if (argc > 2) {
mostrar(total, "total");
}
return erros > 0 ? 1 : 0;
}
Agora é a sua vez: a caixa abaixo é o contador pronto, sem arquivo. Clique em Rodar, digite um texto seu (com acentos), aperte Enter no fim de cada linha e, para terminar, Ctrl+D. Confira as contas à mão com uma frase curta.
#include <stdio.h>
#include <ctype.h>
typedef struct {
long linhas;
long palavras;
long letras;
long caracteres; // como o wc -m: "ã" conta uma vez
long bytes; // como o wc -c: "ã" conta duas
} Contagem;
// No UTF-8, os bytes de 0x80 a 0xBF (10xxxxxx) continuam uma letra
// que começou antes. Todos os outros começam um caractere novo.
bool continua_caractere(int c) {
return (c & 0xC0) == 0x80;
}
// As letras acentuadas do português (á, ã, ç, é, ô, ú...) começam
// todas pelo byte 0xC3 no UTF-8.
bool comeca_letra(int c) {
return isalpha(c) || c == 0xC3;
}
Contagem contar(FILE *f) {
Contagem t = {0};
bool dentro = false;
int c;
while ((c = getc(f)) != EOF) {
t.bytes++;
if (!continua_caractere(c)) {
t.caracteres++;
}
if (comeca_letra(c)) {
t.letras++;
}
if (c == '\n') {
t.linhas++;
}
if (isspace(c)) {
dentro = false;
} else if (!dentro) {
dentro = true;
t.palavras++;
}
}
return t;
}
void mostrar(Contagem t, const char *nome) {
printf("%7ld %8ld %7ld %8ld %7ld %s\n", t.linhas, t.palavras,
t.letras, t.caracteres, t.bytes, nome);
}
void somar(Contagem *total, Contagem t) {
total->linhas += t.linhas;
total->palavras += t.palavras;
total->letras += t.letras;
total->caracteres += t.caracteres;
total->bytes += t.bytes;
}
int main(int argc, char *argv[]) {
printf(" Linhas Palavras Letras Caract. Bytes Arquivo\n");
if (argc == 1) {
mostrar(contar(stdin), "-");
return 0;
}
Contagem total = {0};
int erros = 0;
for (int i = 1; i < argc; i++) {
FILE *f = fopen(argv[i], "r");
if (f == NULL) {
perror(argv[i]);
erros++;
continue;
}
Contagem t = contar(f);
fclose(f);
mostrar(t, argv[i]);
somar(&total, t);
}
if (argc > 2) {
mostrar(total, "total");
}
return erros > 0 ? 1 : 0;
}
O programa completo
A versão final, igual à versão 4. Compile com gcc -std=c23 -Wall -Wextra contar.c -o contar:
#include <stdio.h>
#include <ctype.h>
typedef struct {
long linhas;
long palavras;
long letras;
long caracteres; // como o wc -m: "ã" conta uma vez
long bytes; // como o wc -c: "ã" conta duas
} Contagem;
// No UTF-8, os bytes de 0x80 a 0xBF (10xxxxxx) continuam uma letra
// que começou antes. Todos os outros começam um caractere novo.
bool continua_caractere(int c) {
return (c & 0xC0) == 0x80;
}
// As letras acentuadas do português (á, ã, ç, é, ô, ú...) começam
// todas pelo byte 0xC3 no UTF-8.
bool comeca_letra(int c) {
return isalpha(c) || c == 0xC3;
}
Contagem contar(FILE *f) {
Contagem t = {0};
bool dentro = false;
int c;
while ((c = getc(f)) != EOF) {
t.bytes++;
if (!continua_caractere(c)) {
t.caracteres++;
}
if (comeca_letra(c)) {
t.letras++;
}
if (c == '\n') {
t.linhas++;
}
if (isspace(c)) {
dentro = false;
} else if (!dentro) {
dentro = true;
t.palavras++;
}
}
return t;
}
void mostrar(Contagem t, const char *nome) {
printf("%7ld %8ld %7ld %8ld %7ld %s\n", t.linhas, t.palavras,
t.letras, t.caracteres, t.bytes, nome);
}
void somar(Contagem *total, Contagem t) {
total->linhas += t.linhas;
total->palavras += t.palavras;
total->letras += t.letras;
total->caracteres += t.caracteres;
total->bytes += t.bytes;
}
int main(int argc, char *argv[]) {
printf(" Linhas Palavras Letras Caract. Bytes Arquivo\n");
if (argc == 1) {
mostrar(contar(stdin), "-");
return 0;
}
Contagem total = {0};
int erros = 0;
for (int i = 1; i < argc; i++) {
FILE *f = fopen(argv[i], "r");
if (f == NULL) {
perror(argv[i]);
erros++;
continue;
}
Contagem t = contar(f);
fclose(f);
mostrar(t, argv[i]);
somar(&total, t);
}
if (argc > 2) {
mostrar(total, "total");
}
return erros > 0 ? 1 : 0;
}
O que o projeto juntou: while com getchar e EOF, isspace e isalpha, uma variável de estado bool, structs, argc e argv, fopen e fclose, perror, máscaras de bits e o UTF-8.
Rodando no laboratório
A Ana conferiu uma frase digitada, com quatro letras acentuadas, e olhou os bytes com o xxd:
$ ./contar
Linhas Palavras Letras Caract. Bytes Arquivo
Ação e reação. « Enter e depois Ctrl+D
1 3 11 15 19 -
$ echo 'Ação e reação.' | xxd
00000000: 41c3 a7c3 a36f 2065 2072 6561 c3a7 c3a3 A....o e rea....
00000010: 6f2e 0a o..
11 letras (Ação 4, e 1, reação 6), 15 caracteres (as letras, 2 espaços, o ponto e o Enter) e 19 bytes, porque ç (c3 a7) e ã (c3 a3) aparecem duas vezes cada um, com 2 bytes.
Desafios extras
- Escolher as colunas. Como o
wc, aceite as opções-l,-we-cantes dos nomes dos arquivos e mostre só as colunas pedidas (umargv[i][0] == '-'indica uma opção). - A última linha sem Enter. Conte também a última linha quando o arquivo não termina em
'\n'. Dica: guarde o último caractere lido; no fim, se ele não for'\n'e o arquivo não estiver vazio, some 1. - A linha mais comprida. Mostre o tamanho da maior linha, em caracteres, como o
wc -L. - Palavras sem pontuação. Faça
"caderno,"e"caderno"serem iguais: trate tambémispunct(c)como separador. Quantas palavras dá o poema agora? E"guarda-chuva"? - Frequência das letras. Com um array
long freq[26], conte quantas vezes aparece cada letra de A a Z (sem diferenciar maiúscula de minúscula, comtolower) e mostre a mais comum.
Exercícios
1. Por que a variável que recebe o getchar() deve ser int, e não char?
2. Quantas palavras o contador da versão 2 acha em " bom dia " (com os espaços)?
3. O arquivo oi.txt tem só o texto pão e um Enter. Quantos bytes e quantos caracteres ele tem?
4. No UTF-8, quais bytes continuam um caractere começado antes?
5. Complete o laço que lê a entrada até o fim:
6. Complete o comando que manda o arquivo poema.txt para a entrada do contador:
7. Complete a chamada que lê um caractere do arquivo f, e não do teclado:
Resumo
getcharegetcleem um byte por vez e devolvemint: um byte de 0 a 255 ouEOF. Comchar c, o byte 255 vira o fim do arquivo.- No terminal, a entrada acaba com Ctrl+D; com
< arquivo, o programa lê o arquivo como se fosse o teclado. - Linhas = número de
'\n'; uma última linha sem Enter não conta, nem nowc. - Palavras: conte as passagens de fora para dentro de uma palavra, com uma variável de estado e
isspace. Contar espaços erra com espaços duplos e linhas em branco. - Um
FILE *serve para arquivos e para ostdin; depois de umperror, saia do caminho normal (continueoureturn). - No UTF-8, bytes ≠ caracteres: os bytes
10xxxxxxcontinuam um caractere, e(c & 0xC0) != 0x80acha os começos. - Compare sempre com uma ferramenta de confiança: o
wcmostrou cada erro na hora.
Próximo projeto: Lista de Tarefas.